Skip to content

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 carries Set-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_token of a track's first segment.
  • A discard is an SLTB END record 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/upload reads the whole body without a size limit and has no rate limit.
  • /tracks/upload returns the internal exception message in its 422 body.
  • /tracks/segments can answer 500 after the segment was stored when the tide lookup fails with a network error that OpenMeteoTideService does 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 gets 200 duplicate.