Skip to content

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.

RouteSourceUsed byWhat it does
GET /.well-known/apple-app-site-associationapp/association.rb (JSON built per request)iOS app and App ClipClaims /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.jsonapp/asset_links.rb (JSON built per request, 200, no redirect)AndroidOne 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.hamlBoth apps’ links, when the app is not installed or the link is opened in a browserFallback 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.rbiOS Safariapp-id=1468252627, app-clip-bundle-id=be.fousa.vulture.clip, so Safari can offer the App Clip on a covered /open/ URL
GET /delete-accountapp/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 itRequired 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 /privacyapp/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 /termsviews/terms.hamlBoth apps (Support, paywall footer)Terms of use
GET /faqviews/faq.hamlBoth 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.hamlBoth apps (Support › website)Home page, with the Chrome Web Store link to the “Open in Vulture” extension
/auth/weglideNo routeClaimed in the AASA onlyOld 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.

Every handler under vulture-web/app/api/ starts with authenticate_api! (vulture-web/app/helpers/api_auth.rb):

CheckFailure
The first media type in Accept is exactly application/json406
The Authorization header equals the server’s API_TOKEN environment variable, compared as the raw value. There is no Bearer prefix403

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.

ClientWhere the token comes from
iOSHard-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
AndroidVULTURE_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
ServerENV['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.

The base URL is https://vulture.aero/api/ on both platforms.

EndpointiOS callerAndroid caller
GET soaringspot/countriesSoaringSpotCountriesRequest (vulture-ios/Packages/SoaringSpot/Sources/SoaringSpot/Requests/)HttpSoaringSpotApi (vulture-android/data/soaringspot/)
GET soaringspot/competitions?country=&year=&page=SoaringSpotCompetitionsRequestHttpSoaringSpotApi
GET soaringspot/competitions/:idSoaringSpotCompetitionRequestHttpSoaringSpotApi
GET soaringspot/competitions/:id/pilotsSoaringSpotCompetitionPilotsRequestHttpSoaringSpotApi
GET soaringspot/competitions/:id/flights/:class/:daySoaringSpotCompetitionDayRequest (flights/<dayId>)HttpSoaringSpotApi (the slash in the day id is not encoded)
GET soaringspot/competitions/:id/task/:class/:daySoaringSpotCompetitionDayRequest (task/<dayId>)HttpSoaringSpotApi
GET xcsoar/waypointsFetchFilesViewModel, FileDownloadManager (vulture-ios/Packages/ImportScreens/..., Packages/RemoteContent/...)None
GET xcsoar/airspaceFetchFilesViewModel, FileDownloadManagerXcsoarAirspaceCatalog (vulture-android/data/airspace/)
GET openaip/airspaceFetchFilesViewModel, FileDownloadManagerNone
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 itNone (P7-D18)
GET sgp/eventsNoneNone
GET sgp/events/:idNoneNone
GET sgp/events/:id/flights/:day_idNoneNone

The iOS App Clip uses the same Soaring Spot requests (vulture-ios/Modules/Clip/Sources/Features/Competition/Providers/).

  • /api/open (app/api/open.rb, Open) parses a soaringspot.com competition URL. Both apps now parse Soaring Spot addresses themselves: iOS SoaringSpotCompetition(competitionURL:), and Android OpenLink on 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, and spec/ 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.