API quickstart

Everything lives under https://fitaio.app/api/v1. JSON in, JSON out, snake_case fields, kilograms and centimetres, RFC 3339 timestamps in UTC, days as YYYY-MM-DD.

1. Create a key

In the app, open Settings, then Developer, and create a key. Pick read for tools that only look, or read and write for anything that logs. Choose an expiry of never, 90 days or one year; a key past its expiry stops working on its own, no revoke needed. The key is shown once; it looks like fitaio_ followed by 40 characters. You can hold ten keys and revoke any of them on the same screen.

2. Your first request

GET /me returns the account behind the key.

curl https://fitaio.app/api/v1/me \
  -H "Authorization: Bearer fitaio_YOUR_KEY"
const response = await fetch('https://fitaio.app/api/v1/me', {
  headers: { Authorization: `Bearer ${process.env.FITAIO_API_KEY}` },
});
const me = await response.json();
console.log(me.display_name, me.goal);
import os, requests

headers = {"Authorization": f"Bearer {os.environ['FITAIO_API_KEY']}"}
me = requests.get("https://fitaio.app/api/v1/me", headers=headers, timeout=10).json()
print(me["display_name"], me["goal"])

Response:

{
  "id": "u_5d8e",
  "display_name": "Alex",
  "email": "alex@example.com",
  "height_cm": 181,
  "birth_date": "1994-03-12",
  "gender": "male",
  "goal": "recomp",
  "created_at": "2026-04-02T09:12:00Z"
}

The api-key: fitaio_YOUR_KEY header is accepted as an alternative to the bearer token.

3. Conventions

Topic Rule
Units Weight in kg, lengths in cm, energy in kcal, macros in g. The app converts for display.
Time Timestamps are RFC 3339 in UTC (2026-09-18T17:00:00Z). Day-based resources use YYYY-MM-DD.
Ids Opaque strings. Exercise ids come from GET /exercises; never invent one.
Pagination ?limit= from 1 to 100 (default 25). Responses carry next_cursor; pass it back as ?cursor=. null means the last page.
Filtering since and until on list endpoints, timestamps for workouts and body logs, days for nutrition and supplement logs.
Writes POST creates and returns 201 with the full record, except an idempotent write like a supplement log, which returns 200 on a repeat call. PUT replaces, PATCH changes fields, DELETE returns 204.

4. Page through workouts

const BASE = 'https://fitaio.app/api/v1';
const headers = { Authorization: `Bearer ${process.env.FITAIO_API_KEY}` };

async function* workouts(since) {
  let cursor = null;
  do {
    const url = new URL(`${BASE}/workouts`);
    url.searchParams.set('limit', '100');
    if (since) url.searchParams.set('since', since);
    if (cursor) url.searchParams.set('cursor', cursor);
    const page = await (await fetch(url, { headers })).json();
    yield* page.workouts;
    cursor = page.next_cursor;
  } while (cursor);
}

for await (const workout of workouts('2026-09-01T00:00:00Z')) {
  console.log(workout.started_at, workout.title, `${workout.volume_kg} kg`);
}

Each workout carries its exercises and sets:

{
  "id": "wk_9f2c",
  "title": "Push",
  "notes": "",
  "started_at": "2026-09-18T17:00:00Z",
  "duration_seconds": 3420,
  "volume_kg": 640,
  "plan_id": "pl_1a2b",
  "plan_name": "Lean Cut PPL",
  "exercises": [
    {
      "exercise_id": "barbell-bench-press",
      "name": "Barbell Bench Press",
      "muscle_group": "Chest",
      "notes": "",
      "superset_id": null,
      "sets": [
        { "type": "warmup", "weight_kg": 40, "reps": 10, "completed": true, "rpe": null },
        { "type": "working", "weight_kg": 80, "reps": 8, "completed": true, "rpe": 8 }
      ]
    }
  ],
  "created_at": "2026-09-18T18:02:11Z",
  "updated_at": "2026-09-18T18:02:11Z"
}

5. Log a workout

Find the exercise id first, then post the session. Set type defaults to working; warm-ups count for nothing.

curl "https://fitaio.app/api/v1/exercises?q=bench&limit=3" \
  -H "Authorization: Bearer fitaio_YOUR_KEY"
curl -X POST https://fitaio.app/api/v1/workouts \
  -H "Authorization: Bearer fitaio_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Push",
    "started_at": "2026-09-19T17:00:00Z",
    "duration_seconds": 3300,
    "exercises": [
      {
        "exercise_id": "barbell-bench-press",
        "sets": [
          { "type": "warmup", "weight_kg": 40, "reps": 10 },
          { "weight_kg": 82.5, "reps": 8, "rpe": 8 },
          { "weight_kg": 82.5, "reps": 8, "rpe": 8.5 },
          { "weight_kg": 82.5, "reps": 7, "rpe": 9 }
        ]
      }
    ]
  }'

The response is 201 Created with the stored workout, including volume_kg computed from the completed working sets. Writes need a key with read and write scope.

6. Errors

Every error uses one envelope:

{
  "error": {
    "code": "invalid_request",
    "message": "The request is invalid.",
    "details": [{ "path": "body.exercises[0].sets[1].reps", "message": "is required" }]
  }
}
Status Code Meaning
400 invalid_request A field is missing or malformed; details lists each problem
401 invalid_key Missing, malformed, revoked or unrecognized key
401 expired_key The key was created with an expiry and that date has passed
403 insufficient_scope A write with a read-only key
404 not_found No such record in this account
405 method_not_allowed Wrong HTTP method for the path
413 payload_too_large Body over 256 KB
415 unsupported_media_type Send Content-Type: application/json
429 rate_limited Over 120 requests a minute for this key, or 300 for this IP
503 unavailable Temporary; retry after a moment

7. Rate limits

120 requests a minute per key and 300 a minute per IP address. A 429 carries the rate_limited code and a Retry-After: 60 header; wait a minute and continue. Use limit=100 on list endpoints and the since filter to keep request counts low.

8. Coming from Hevy

Scripts that send api-key: <key> work without changes to the header. Differences to plan for: every weight is in kilograms, exercise ids are Fitaio’s own (search GET /exercises?q=), and there is no bulk import endpoint; post workouts one at a time.

Next

  • Recipes: export everything, log from a script, sync weight, weekly report.
  • MCP server: let Claude, ChatGPT, Cursor or VS Code use the same data.
  • API reference: every endpoint and schema.