# Training-plan contract

Base URL: `https://labs.almeidaracingacademy.com`.

The API serves challenge definitions to eligible Gold members, including the Academy's Gold bundles, lifetime plans and Platinum plans. Each server request verifies an ARA session and membership; membership checks cache for up to 60 seconds. Current trialing, subscribing, cancelling (until the term ends), and past-due accounts follow the Academy's existing access policy. Expired and non-qualifying accounts are denied.

## Desktop login

No secret, Outseta key, Garage61 key, localhost listener, or custom URL scheme is needed in the app. This is ARA's transaction-based desktop login protocol, not Outseta's device-authorization grant.

1. Generate a fresh cryptographically random `code_verifier` (43–128 characters using `A–Z a–z 0–9 - . _ ~`). Keep it in application memory. Compute `code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))` without padding.
2. `POST /api/v1/auth/transactions` with JSON:

```json
{
  "client_id": "ara-challenge-overlay",
  "code_challenge_method": "S256",
  "code_challenge": "YOUR_43_CHARACTER_S256_CHALLENGE"
}
```

The `201` response contains `transaction_id`, `user_code`, `verification_uri`, `expires_in: 600`, and `interval: 5`. These are generated afresh for every login. The client identifier is public.

3. Display `user_code` in the overlay and open `verification_uri` in the system browser. The member enters that code and signs in with the Academy. Tell members never to enter a code sent by another person. Sign-in happens on `https://almeidaracingacademy.com/sign-in`, which returns the browser to Labs. Labs verifies the Academy session and Gold membership on the server. The overlay never receives the Academy cookie or provider credentials.
4. Poll `POST /api/v1/auth/token` at most once every five seconds, with JSON containing `transaction_id` and the original `code_verifier`. `202` with `status: "authorization_pending"` means wait. `429` means obey `Retry-After` (wait 60 seconds if absent). On `503`/`504`, retry the same transaction with bounded backoff; an approved login is retained if session creation has not committed. Stop after ten minutes, on cancellation, or on a terminal error; start a new transaction if necessary.
5. A successful `200` returns `access_token`, `token_type: "Bearer"`, `expires_in: 43200`, and `scope: "training-plans:read"`. The transaction is consumed exactly once. Never retry a successful redemption. If its response is lost, start another login.
6. Use `Authorization: Bearer <access_token>` for API requests. The token is an opaque ARA credential, not an Outseta token. Keep it in memory or the operating system's credential store (Windows Credential Manager/DPAPI). Never put it in a URL, application log, source file or shared config.

`GET /api/v1/auth/session` checks the session and returns `authenticated`, `entitlements` and `expires_at`. `DELETE /api/v1/auth/session` revokes the bearer session and returns `204`; also clear local credentials. This signs out this app, not the member's Academy browser session.

Sessions last 12 hours. There is no refresh token: repeat login after expiry or a `401`. No provider access or refresh token is stored in the application or returned to it. A `403` requires an eligible membership; do not repeatedly retry it.

[Download a runnable Node.js reference client](/docs/overlay-client.mjs). It demonstrates login, plan reads and revocation without printing or persisting access tokens. Run with Node.js 24: `node overlay-client.mjs`.

### Authentication errors

Auth responses use an `error` string, a `message` and a `requestId` where available. Token polling may return `invalid_grant` (bad proof), `expired_token` (expired/used transaction), `access_denied`, `membership_required`, `temporarily_unavailable`, or `slow_down`. `202` is a pending status, not an error. `401` rejects invalid sessions; `403` denies membership or an unapproved browser origin; `410` means start a new login; `429` means wait. `503` indicates an outage or a paused release. None of these should become an empty successful challenge list.

## Collection

`GET /api/v1/garage61/training-plans?limit=100&offset=0`

Returns `{ team, items, total, limit, offset }`. Limits are integers 1–1000;
offset is a nonnegative integer. Team defaults to `almeida-racing-academy`
and must belong to the configured allowlist.

## Detail

`GET /api/v1/garage61/training-plans/{trainingPlanId}`

Returns `{ trainingPlanId, team, mode: "plan", trainingPlan }`.
The plan retains provider metadata and content. Each content item has
`trackInfo`, `carInfos` and `result: null`. No personal laps/results are fetched.
`mode=full`, `fresh=1` and website `pageUrl` parameters do not alter this behavior.

Match `trackInfo.platform_id` and `carInfos[].platform_id` to iRacing numeric
IDs after checking `platform === "iracing"`. Garage61 IDs are different.
An empty `cars` list means all cars; nonempty `cars` with missing mappings is
an error the overlay must handle. Medal targets use labels and numeric seconds;
do not infer medal type from array position.

Weekly date/weather fields are not guaranteed by the published Garage61 schema.
Preserve and inspect actual provider metadata; never infer a weekly window from
creation/update timestamps. Local progress should be keyed by plan and content
IDs. Validate actual session conditions before awarding challenge medals.

## Polling

Fetch on startup and at most every five minutes while in use. Plans cache for
five minutes and car/track catalogs for an hour. Limits are 120 requests/minute
per IP before auth and 60/minute per user; these are protective ceilings, not
recommended polling rates. Respect `Retry-After` on 429 and use bounded retries
with jitter on temporary failures. Vercel also applies a 240 requests/minute
per-IP, per-region limit across API paths before function execution.

## Errors

Application errors contain an `error` string. Some also include a `requestId` for support. Edge/platform errors, including rate-limit responses, may use a non-JSON body. Check HTTP status before parsing; on 429 respect `Retry-After`, or wait 60 seconds if absent.

A 503 means the service is temporarily unavailable or paused. Use bounded retries; do not retry continuously.

502 includes Garage61 credential failures or malformed provider data; do not ask the member to sign in again for these errors.

401 means authentication is required. 403 means an eligible Gold membership is required.
400 means invalid input; 405 means unsupported method. Upstream failures must
not become empty successful responses.


Other 5xx responses indicate service or provider failures. 504 means the request exceeded its time budget. Some provider statuses, including 404, may be forwarded. Unknown content types and extra fields should be tolerated.

## Client security

Keep user access tokens in the operating system credential store. Send them only in the Authorization header; never include them in URLs or logs. The app must not contain an Outseta OAuth client secret, an administrative API key, or the Garage61 team token. The desktop flow above is the supported way to get an ARA session.

Native desktop clients and same-origin Labs pages do not need CORS configuration. Other browser origins are not currently enabled.

## Machine-readable contract

[OpenAPI specification](/openapi.json) · [AI documentation index](/llms.txt) · [Developer docs](/docs)

Examples in the specification are synthetic, not real challenges or official medal times.
