# Airon > Airon is a training assistant for coaches and their clients. Coaches plan, monitor and message > athletes; the assistant drafts plans, flags exceptions and explains; the coach or athlete decides. > Athletes use it free. Coach site: https://airon.coach/ (English: https://airon.coach/en/). > Airon exposes a PUBLIC API and an MCP server so an agent that can issue HTTP requests can > log and read a user's training and nutrition on their behalf — no scraping, no browser > automation, no password sharing. > AUTH: OAuth 2.1 (PKCE S256) OR a static bearer token `airon_pat_…`. > ChatGPT and Claude custom connectors: paste `https://airon.app/api/functions/mcp` > (Settings → Connectors). They speak OAuth 2.1; discovery is RFC 9728 via the > `resource_metadata` URL in the 401's `WWW-Authenticate` header. Cursor/scripts may set > `Authorization: Bearer airon_pat_…` instead. > This API is the connected USER's account, not their trainees. > Progress photos are NOT in the OAuth grant (`photos:*` is PAT-only). IMPORTANT FOR AI AGENTS: this file replaces the auto-generated page index. Most Airon URLs are private, authenticated app screens with `noindex` — do not crawl or link them. Use the API below instead of the UI. Public, linkable pages are listed at the bottom. ## Connect a user's account (no Airon account needed by you) Two ways to get a token. Prefer the pairing flow — the user never copies a secret. Both require that YOU can issue HTTP requests. If you cannot, say so and ask the user to mint a token themselves (option B) for a client that can. ### A. Pairing flow (recommended when you can call HTTP) 1. Start — no auth required: ``` POST https://airon.app/api/functions/authorizeApiClient Content-Type: application/json {"action":"start","client_name":"Your client name","scopes":["profile:read","workouts:read","workouts:write","nutrition:read","nutrition:write","metrics:read","metrics:write"]} ``` Returns `user_code`, `device_code`, `connect_url` and `expires_in` (900 seconds). 2. Tell the user to open `connect_url` (https://airon.app/ApiAccess?code=XXXX-XXXX) and approve. They must be signed in to Airon; the approval screen shows your `client_name` and the exact permissions you asked for, and they may grant fewer than you requested. 3. Poll every 3 seconds with the `device_code` you kept: ``` POST https://airon.app/api/functions/authorizeApiClient {"action":"poll","device_code":"airon_dev_…"} ``` `202 authorization_pending` = keep waiting. `200` returns `access_token` ONCE — store it. `410 expired` / `410 already_claimed` = start a new pairing. Pairing tokens never expire. ### B. Manual token The user creates one at https://airon.app/ApiAccess and pastes it to you. ## Authentication Every API call: ``` Authorization: Bearer airon_pat_… ``` The token identifies the user. There is no user/email parameter anywhere in this API — supplying one does nothing. A token is limited to the scopes its owner granted; calling outside them returns REST 403 with `required_scope` naming what is missing (over MCP the same refusal is HTTP 200 `isError` — see Errors). Missing/invalid MCP credentials are HTTP 401; rate limits are HTTP 429. Rate limit: 100 requests per token per FIXED 60-second window; REST answers 429 `rate_limited` with `retry_after_seconds`. The window resets on the first request after it lapses (it does not slide), the counter is written without blocking the request so it is best-effort under concurrency, and the hosting platform applies its own limits in front of this one. Treat 100 as a ceiling to stay under, not an allowance to spend. The limit is counted per token, not per account. ## MCP server ``` https://airon.app/api/functions/mcp ``` JSON-RPC 2.0 over HTTP. Methods: `initialize`, `tools/list`, `tools/call`, `ping`. Two authentications, both first-party (not Base44 App MCP): 1. **OAuth 2.1** — ChatGPT / Claude custom connector. Paste the URL above into Settings → Connectors. Discovery: HTTP 401 + `WWW-Authenticate` → `resource_metadata` `https://airon.app/api/functions/oauth?p=resource` (RFC 9728) → RFC 8414 at `https://airon.coach/.well-known/oauth-authorization-server` (issuer `https://airon.coach`; authorize/token on airon.app). PKCE S256 is required. Consent is `/AuthorizeApp`. OAuth tokens expire in 30 days. Photos are not granted. 2. **Bearer PAT** — Cursor / scripts. `Authorization: Bearer airon_pat_…` minted at https://airon.app/ApiAccess. `tools/list` returns only the tools the token's scopes permit. `GET` the same URL with `?descriptor=1` for a capability descriptor (a bare `GET` with no `Authorization` header is the 401 challenge). `initialize` is answered whenever an `Authorization` header is present, valid or not (with none at all it is the 401 challenge), so a successful handshake does not mean the token is valid — call `tools/list` to find out. This is the connected user's own training and nutrition. Coaches do not get trainee tools. ## REST API Base: `https://airon.app/api/functions/apiV1` `GET` the base URL (no auth) for a machine-readable index of every endpoint and its JSON schema. OpenAPI 3.1: `?path=/openapi.json`. Calling convention: the resource goes in the `path` query parameter; filters and dates are ordinary siblings of it — `GET …/apiV1?path=/workouts&from=2026-08-01&to=2026-08-07`. Sub-paths (`…/apiV1/workouts`) are NOT routed and return 404. | Method | Path | Scope | Purpose | |---|---|---|---| | GET | /profile | profile:read | Goals, units, body profile, daily calorie + macro targets. No email address | | GET | /workouts | workouts:read | List sessions and plans in a date range (capped, see below) | | GET | /workouts/{id} | workouts:read | One session with its sets / intervals | | POST | /workouts | workouts:write | Log a workout (strength, cardio, rest, RPE) | | PATCH | /workouts/{id} | workouts:write | Edit a session (sending `exercises` replaces the list) | | DELETE | /workouts/{id} | workouts:write | Soft-delete a session | | GET | /meals | nutrition:read | Meals + daily totals | | POST | /meals | nutrition:write | Log food (one entry per item) | | DELETE | /meals/{id} | nutrition:write | Delete a meal entry | | POST | /water | nutrition:write | Log hydration in ml | | GET | /metrics | metrics:read | Weight, body fat, circumferences. Photo URLs need photos:read too | | POST | /metrics | metrics:write | Log a measurement. Attaching a photo needs photos:write too | | DELETE | /metrics/{id} | metrics:write | Delete a measurement. A photo entry needs photos:write too | ### Response caps — every list is a window and NONE returns a cursor Do not describe a list response as the user's complete history. There is no `has_more` and no `next_cursor`. `/workouts`, `/meals` and `/metrics` carry `truncated`: true when the response is not the whole requested range (more sessions than `limit`, or a range reaching past the read window), with a `note` naming the dates that may be missing. `truncated: false` means the window covered the range, not that the account holds nothing else (see the target-field caveat below). - `/workouts` reads the newest 500 sessions recorded against the account and then filters THAT set by date. A range older than the window returns empty rather than paginated. It matches the account's target field only, so an older session recorded under just its creator can be missing at any date. - `/workouts?include_sets=true` and `/workouts/{id}` return a session's live sets, read from its newest 500 set rows. - `/meals` and `/metrics` read up to 500 rows per ownership field and merge the two, so the merged result is not date-ordered. ### Progress photos are a SEPARATE permission `metrics:read` and `metrics:write` cover measurements only. Progress photos moved to their own `photos:read` / `photos:write` scopes on 2026-09-06, because the stored photo URLs are public, unsigned and permanent — passing one on is passing on the image itself, for good. - Do NOT add `photos:read` to a pairing request by default. Ask for it only when the user has said they want an assistant looking at their progress photos, and ask for it separately. - Without it, `/metrics` still returns the photo ENTRY (id, date, notes) with `"photo_url": null` and `"photo_url_withheld": true`, plus a `photos_withheld` summary. `photo_url_withheld` means "you may not see this", NOT "there is no photo" — never report it to the user as an entry without a photo. - `photos:write` is required to attach a photo (HTTPS only) and to delete a photo entry. That delete is permanent and there is nothing to restore it from. If the user asks for a total, an average or "all my history", say which window you actually read. Do not present a capped read as an export. ### Units — convert before sending, these are not negotiable - weight: **kilograms** · distance: **metres** (5 km = 5000) · pace: **seconds per km** (5:30/km = 330) - body lengths: **centimetres** · duration + rest: **seconds** · dates: **YYYY-MM-DD** - `GET /profile` tells you the user's *display* preference (metric/imperial) so you can talk to them in their units while still sending canonical ones. ### Logging a workout Send ONE entry per set. Three sets of bench press = three entries with `set_number` 1, 2, 3. Cardio uses the same array with `activity_type` set to run/bike/swim/walk/row/hike/elliptical/ski/other. ``` POST https://airon.app/api/functions/apiV1?path=/workouts Authorization: Bearer airon_pat_… { "date": "2026-08-02", "title": "Push day", "exercises": [ {"exercise_name":"bench press","set_number":1,"reps":8,"weight":80,"rpe":7,"rest_seconds":120}, {"exercise_name":"bench press","set_number":2,"reps":8,"weight":80,"rest_seconds":120}, {"exercise_name":"treadmill run","activity_type":"run","set_number":1, "distance_m":5000,"duration_seconds":1500,"avg_hr":152,"avg_pace_seconds_per_km":300} ] } ``` `"completed": false` creates a PLANNED session for a future date instead of a performed one. ## Token lifecycle (reauth / rescope / revoke) The HTTP statuses in this section are the REST contract. Over MCP, auth is HTTP 401 and rate limits are HTTP 429; scope/tool failures stay HTTP 200 (see Errors). - Tokens are bearer secrets, not sessions. There is NO refresh flow. - `invalid_token` / `revoked_token` / `expired_token` (REST 401) → the credential is dead. Start a NEW pairing (section A) and have the user approve again. Never loop-retry it. - Rescope: for a manual or paired token, the user edits it in place at https://airon.app/ApiAccess (pencil icon) — the token string does not change. An OAuth row cannot gain new permissions that way (including photos) — disconnect and approve again. Or re-pair with the extra scopes for a new token. - An `insufficient_scope` response (REST 403) names the missing scope in `required_scope`. Tell the user exactly which permission to enable; do not retry until they have. - Revocation is instant at /ApiAccess and takes effect on the next request. - The user is emailed the first time a token is used, so silent connections cannot stay silent. ## Errors The two transports report failure DIFFERENTLY. Branch on the `error` code, never on the HTTP status, and never assume the status you get on REST is the one MCP will send. ### REST (`apiV1`) — the HTTP status is real Every REST error body is `{"ok":false,"error":,"message":}`. | Code | HTTP | What to do | |---|---|---| | missing_token / invalid_token | 401 | Run the pairing flow. | | revoked_token / expired_token | 401 | Re-pair; only the user can re-approve. | | insufficient_scope | 403 | Read required_scope; ask the user to grant it at /ApiAccess. | | rate_limited | 429 | Wait retry_after_seconds. | | unknown_endpoint | 404 | Wrong path — GET the base URL; use ?path=, not sub-paths. | | not_found | 404 | No such id on this account (other accounts' ids look identical — that is deliberate). | | api_write_unavailable | 503 | Temporary; reads still work. | | idempotency_check_unavailable | 503 | Nothing was saved; retry the same call with the same client_request_id. | | idempotency_key_reused | 409 | This client_request_id already belongs to a different entry (named in the message). Use a new key for a new entry. | | replay_incomplete | 409 | This client_request_id already created a workout that holds fewer sets than sent. Retry later with the same key; do not edit or delete it while the first call may still be saving. | | internal_error | 500 | Nothing was saved; retry once. | ### MCP — auth is HTTP 401, rate limit is HTTP 429, tools stay 200 Unauthenticated `tools/list` / `tools/call` (and `initialize` with no `Authorization` header) → HTTP 401, with JSON-RPC `error` code `-32001` in the body and this one-line header (as sent by airon.app and airon.coach), wrapped here: ``` WWW-Authenticate: Bearer realm="Airon", resource_metadata="https://airon.app/api/functions/oauth?p=resource", authorization_uri="https://airon.coach/.well-known/oauth-authorization-server" ``` Follow `resource_metadata` exactly as given; do not derive it from the host you called. Do not build a `.well-known` URL yourself either. On airon.app, the one under the MCP path answers 404, and the root `/.well-known/oauth-protected-resource` is Base44's `/api/mcp` server, not this one. Rate limiting → HTTP 429 (do not start OAuth again). Scope and tool failures stay HTTP 200: 1. Missing scope → a normal result with `isError:true` whose `content[0].text` is the `{"ok":false,"error":…}` body as a JSON string. 2. Tool failure → a result with `isError:true` whose `content[0].text` is `{"error":,"message":}` — with NO `ok` field. Parse defensively. An unknown method returns a JSON-RPC `-32601` error, also under HTTP 200. ## Rules for agents - Never invent numbers the user did not give you. Ask for the weight rather than guessing it. - Confirm what you logged. `log_workout` returns a `view_url` that opens the saved session; show it. - An `insufficient_scope` refusal is not a retry — tell the user which permission to grant. - Deletions are the user's decision, not yours. Ask first. - Never call a capped list response "everything" or "all your history". Say what you read. - This data is health-related. Reading it moves the user's training, nutrition, body measurements and progress-photo links to whoever operates you. Do not forward it further, and point the user at https://airon.app/PrivacyPolicy (section 6) if they ask who receives it. ## Docs - Developer documentation: https://airon.app/Developers - Manage tokens: https://airon.app/ApiAccess - Privacy policy: https://airon.app/PrivacyPolicy - Terms of service: https://airon.app/TermsOfService ## Public pages - [Home](https://airon.app/): the training assistant for coaches and their clients - [For coaches](https://airon.coach/): coaching and team features (the coach site; English at https://airon.coach/en/) - [Pricing](https://airon.app/Pricing) - [Developers](https://airon.app/Developers): full API + MCP documentation - [Find a coach](https://airon.app/TeamDirectory): public directory of coaching teams - [Blog](https://airon.app/Blog): training and nutrition articles - [Privacy policy](https://airon.app/PrivacyPolicy) - [Terms of service](https://airon.app/TermsOfService) - [Account deletion](https://airon.app/AccountDeletion)