Routes the apps depend on
Applies to web only
As of 2026-10-19 (vulture-web 61d304e, vulture-ios 3176c4cc4, vulture-android 85fd9cc6).
vulture-web is a Sinatra app (vulture-web/app.rb) that registers its modules from vulture-web/app/. Production forces SSL. The JSON endpoints are documented per endpoint in API Documentation. This page covers what that set leaves out: the site routes that the apps depend on, and who calls what.
Non-API routes the apps depend on
Section titled “Non-API routes the apps depend on”| Route | Source | Used by | What it does |
|---|---|---|---|
GET /.well-known/apple-app-site-association | app/association.rb (JSON built per request) | iOS app and App Clip | Claims /open/* and /auth/weglide for 743AM63YFU.be.fousa.vulture and .debug. Names the clips 743AM63YFU.be.fousa.vulture.clip and .debug.clip. See Deep links |
GET /.well-known/assetlinks.json | app/asset_links.rb (JSON built per request, 200, no redirect) | Android | One handle_all_urls statement for be.fousa.vulture (release fingerprint) and one for be.fousa.vulture.debug (debug fingerprint). A package with no fingerprints gets no statement. Specs in spec/app_spec.rb |
GET /open/* | app/public.rb, views/open.haml | Both apps’ links, when the app is not installed or the link is opened in a browser | Fallback page: the app icon, “Open in Vulture”, the App Store badge, the line “Free with optional subscription”, and a QR code of the current URL (made client-side with qrcode-generator@1.4.4 from jsDelivr). It has no Google Play badge or Android hint yet (ios-changes A5). The redesign branch adds the Google Play badge, the visitor’s store first, and on a phone hides the QR code for an install line (Website) |
Smart App Banner (in views/partials/head.haml, every page) | app/helpers/app_store.rb | iOS Safari | app-id=1468252627, app-clip-bundle-id=be.fousa.vulture.clip, so Safari can offer the App Clip on a covered /open/ URL |
GET /delete-account | app/public.rb, views/delete_account.haml, locales/*.yml delete_account.* | Google Play (the data-deletion URL in the store listing). The app itself does not link to it | Required by Google Play. It explains in-app deletion on Android, deletion by e-mail to hello@fousa.be (the owner deletes these by hand, P8B-D13), what is deleted and what is kept. The subscription must be cancelled in Google Play |
GET /privacy | app/public.rb, views/privacy.haml, locales/*.yml privacy.* | iOS (Settings › Support, paywall footer), Android (SupportSections.kt, PaywallScreen.kt, AccountScreen.kt) | Privacy policy. It covers the browser extensions (chrome_title / chrome_text) and the Android section “Sync and your Google account (Android)” (android_title / android_text: Firebase, region europe-west1, Crashlytics, RevenueCat by the Firebase uid) |
GET /terms | views/terms.haml | Both apps (Support, paywall footer) | Terms of use |
GET /faq | views/faq.haml | Both apps (Support) | FAQ. No Chrome-on-Android entry yet (A5), on the redesign branch neither; that branch adds the Android answers (devices, sync, Pro across stores) |
GET / | views/index.haml | Both apps (Support › website) | Home page, with the Chrome Web Store link to the “Open in Vulture” extension |
/auth/weglide | No route | Claimed in the AASA only | Old WeGlide OAuth redirect (vulture-ios/Packages/Preferences/Sources/Preferences/Vulture.swift). WeGlide now uses an API key. A candidate for removal from the AASA |
/sitemap.xml lists /, /features, /why, /faq, /terms, /privacy and /delete-account, each with an hreflang alternate per site locale. /features, /why and /blog* are marketing pages that no app depends on. The redesign branch adds /download (both stores, see Website) to the routes and the sitemap.
Authentication of /api/*
Section titled “Authentication of /api/*”Every handler under vulture-web/app/api/ starts with authenticate_api! (vulture-web/app/helpers/api_auth.rb):
| Check | Failure |
|---|---|
The first media type in Accept is exactly application/json | 406 |
The Authorization header equals the server’s API_TOKEN environment variable, compared as the raw value. There is no Bearer prefix | 403 |
There is one shared token for every client. It is a static value shipped inside both apps, so it identifies the apps, not users. It is not a secret against someone who unpacks an app. The token value is not reproduced here.
| Client | Where the token comes from |
|---|---|
| iOS | Hard-coded in vulture-ios/Packages/Backend/Sources/Backend/VultureServiceConfiguration.swift (the Authorization header). There is no certificate pinning: the doc comment there says it was dropped after outages on 2026-07-16 and 2026-09-14 |
| Android | VULTURE_API_TOKEN from local.properties or the CI environment, written into BuildConfig.VULTURE_API_TOKEN (vulture-android/core/network/build.gradle.kts). It is added by VultureBackendInterceptor. The request logger never logs it |
| Server | ENV['API_TOKEN'] |
The roadmap lists an optional token per platform as possible future work (vulture-android/docs/analysis/00-roadmap.md §4). Nothing is built.
When UMAMI_WEBSITE_ID is set, every /api/* request is also counted in Umami as event api_request, with method and status (vulture-web/app/helpers/umami.rb). Public pages detect the locale from the browser. /api paths skip that step.
Endpoint → caller
Section titled “Endpoint → caller”The base URL is https://vulture.aero/api/ on both platforms.
| Endpoint | iOS caller | Android caller |
|---|---|---|
GET soaringspot/countries | SoaringSpotCountriesRequest (vulture-ios/Packages/SoaringSpot/Sources/SoaringSpot/Requests/) | HttpSoaringSpotApi (vulture-android/data/soaringspot/) |
GET soaringspot/competitions?country=&year=&page= | SoaringSpotCompetitionsRequest | HttpSoaringSpotApi |
GET soaringspot/competitions/:id | SoaringSpotCompetitionRequest | HttpSoaringSpotApi |
GET soaringspot/competitions/:id/pilots | SoaringSpotCompetitionPilotsRequest | HttpSoaringSpotApi |
GET soaringspot/competitions/:id/flights/:class/:day | SoaringSpotCompetitionDayRequest (flights/<dayId>) | HttpSoaringSpotApi (the slash in the day id is not encoded) |
GET soaringspot/competitions/:id/task/:class/:day | SoaringSpotCompetitionDayRequest (task/<dayId>) | HttpSoaringSpotApi |
GET xcsoar/waypoints | FetchFilesViewModel, FileDownloadManager (vulture-ios/Packages/ImportScreens/..., Packages/RemoteContent/...) | None |
GET xcsoar/airspace | FetchFilesViewModel, FileDownloadManager | XcsoarAirspaceCatalog (vulture-android/data/airspace/) |
GET openaip/airspace | FetchFilesViewModel, FileDownloadManager | None |
GET elevations/:tile (302 to the S3 .hgt.zip) | ElevationRequest (vulture-ios/Packages/Flights/Sources/Flights/Service/) | HttpElevationProvider (vulture-android/data/elevation/), 10 s budget |
GET skysight (localised by Accept-Language) | SkySightCatalogueRequest (vulture-ios/Packages/SkySight/.../Catalogue/) | None: SkySight is out of scope for v1 |
GET open?url= | None. SoaringSpotOpenRequest exists but nothing calls it | None (P7-D18) |
GET sgp/events | None | None |
GET sgp/events/:id | None | None |
GET sgp/events/:id/flights/:day_id | None | None |
The iOS App Clip uses the same Soaring Spot requests (vulture-ios/Modules/Clip/Sources/Features/Competition/Providers/).
Endpoints without a caller
Section titled “Endpoints without a caller”/api/open(app/api/open.rb, Open) parses a soaringspot.com competition URL. Both apps now parse Soaring Spot addresses themselves: iOSSoaringSpotCompetition(competitionURL:), and AndroidOpenLinkon the page addresses. It has a spec (spec/)./api/sgp/*(app/api/sgp.rb,app/api/sgp_flights.rb, SGP) serves Sailplane Grand Prix data through crosscountry.aero. Neither app has an SGP screen, andspec/has no SGP spec.
Both can stay while they cost nothing. If they are removed, delete their pages under docs/api/vulture-web/ in the same change. Third-party services that the apps call directly, not through vulture-web, are in Integrations.
