API (/api/v1)¶
| Server | |
|---|---|
| Status | ✅ |
| Code | config/routes.rb (namespace :api), app/controllers/api/v1/ |
Verified against code: 2026-10-11
Purpose¶
The API is how watches talk to the server: register, pair, upload tracks and get tides back. It is small on purpose. Everything else (trips, boats, editing) happens on the website.
Conventions¶
- Base path
/api/v1, no locale prefix. Production base URL:https://api.sailever.com/api/v1(release builds of the Garmin and Wear OS watch apps). - Controllers inherit from
ActionController::API: no session, cookies, CSRF or layouts (ADR 001). The session is skipped, so no response carriesSet-Cookie. The API answers only on the API host (README.md). - Request and response bodies are JSON, except the FIT upload body.
- Errors are
{"error":"<message>"}unless stated otherwise. - Authentication:
Authorization: Bearer <device_token>(device-api.md).
Debug clients¶
| Client | Debug base URL |
|---|---|
| Garmin simulator | http://127.0.0.1:3000/api/v1 |
| Wear OS emulator | http://10.0.2.2:3000/api/v1 |
Endpoints¶
| Method | Path | Auth | Purpose | Details |
|---|---|---|---|---|
POST |
/devices/register |
none | Install and version check-in | device-api.md |
POST |
/devices/pairing_requests |
none | Get a 6-digit code and poll token | device-api.md |
GET |
/devices/pairing_requests/poll |
poll token | Wait for the code to be entered, receive the device token | device-api.md |
POST |
/devices/unpair |
bearer (optional) | Revoke this device's token | device-api.md |
POST |
/tracks/segments |
bearer | Upload one SLTB segment, get tides back | track-sync.md |
POST |
/tracks/upload |
bearer | Upload one FIT file | below |
There is nothing else: no live positions, no trip, discard, pause or alert calls, no tide endpoint, no boat data.
- A trip is created from the
trip_tokenof a track's first segment. - A discard is an SLTB
ENDrecord with reason 1. - Tides come back in the segment upload response.
POST /tracks/upload (FIT file)¶
Uploads one binary .fit file for the authenticated device.
| Headers | Authorization: Bearer <device_token>, Content-Type: application/x-fit, optional X-Trip-Token |
| Body | Raw FIT bytes |
| Trip token | The FIT session's trip_token developer field wins; else X-Trip-Token, else the trip_token query parameter |
The file is decoded and imported synchronously by Ingestion::Pipeline with source direct_sync (ingestion.md).
| Status | Body | Meaning |
|---|---|---|
200 |
{"track_id":"<uuid>","status":"processed"} |
Imported, or the same recording already exists (same device and start time); then the existing track's id is returned. |
400 |
{"error":"Empty FIT binary payload"} |
Empty body |
401 |
see authentication | |
422 |
{"error":"Failed to process FIT track log: <message>"} |
Any error during decoding or import |
No watch app calls this endpoint today. It has no size or rate limit.
Rate limits¶
See device-api.md. Exceeding a limit answers 429.
Known deviations¶
/tracks/uploadreads the whole body without a size limit and has no rate limit./tracks/uploadreturns the internal exception message in its422body./tracks/segmentscan answer500after the segment was stored when the tide lookup fails with a network error thatOpenMeteoTideServicedoes not catch (only timeouts, DNS errors and bad JSON are caught; e.g. connection refused or TLS errors are not). The watch then keeps the file; the resend gets200 duplicate.