API Reference

Field-by-field reference for developers wiring up an integration. For what the API is for and how to get a key, see Integrations & API. This page assumes you already have one.

Authentication

Every request needs a bearer token, generated by an org admin from Admin Panel → API Access. The base URL is https://forzara.ai/api/v1/.

Authorization: Bearer fz_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

A key is scoped to exactly one organization and to whichever resources it was granted (roster.read, roster.read.email, milestones.read, meetings.read) — there is no way to request data outside the org that issued the key. A key may also be IP-restricted and/or set to expire; ask your admin if a request is unexpectedly rejected.

Roster

GET /roster
GET /roster/{user_id}

Every active org member — participants and staff alike. Scope: roster.read.

FieldTypeNotes
user_idintegerStable identifier — use this to filter /milestones and /meetings.
full_namestring
emailstringOnly present if the key also carries roster.read.email — see the note below.
rolestringclient, staff, or admin.
statusstringactive, completed, or exited.
coach_idinteger or nullAssigned coach's user_id, if any.
joined_atstringISO 8601 UTC.
updated_atstringISO 8601 UTC — last change to this member's row (role, status, coach, or a fresh join).

roster.read alone returns everything above except email. A key needs the additional roster.read.email scope for email addresses to appear — an integration that only needs headcounts, status, or roles (a BI dashboard, say) can be granted a key that never sees contact info at all. Granting roster.read.email without roster.read does nothing on its own.

{
  "data": {
    "items": [
      { "user_id": 7, "full_name": "Joe Smith", "email": "joe@example.org",
        "role": "client", "status": "active", "coach_id": 2,
        "joined_at": "2026-07-31T06:49:36Z", "updated_at": "2026-07-31T06:49:36Z" }
    ],
    "pagination": { "page": 1, "per_page": 100, "total": 1, "total_pages": 1 }
  },
  "meta": { "resource": "roster", "version": "v1" }
}

(Example shown with roster.read.email granted — omit that scope and the email key is absent from each item.)

Milestones

GET /milestones

No single-item form — a milestone is an aggregate of two independent lists, each separately paginated. Filter either or both with ?participant_user_id=. Scope: milestones.read.

Case notes and review notes are never returned by this endpoint — status only, always.

FieldWhereNotes
user_idboth
milestone_keyboth
completed_oncompletionsCalendar date (YYYY-MM-DD), no time component.
registered_atcompletionsISO 8601 UTC.
statussubmissionspending, approved, or denied.
submitted_at / reviewed_atsubmissionsISO 8601 UTC. reviewed_at is null until reviewed.
{
  "data": {
    "completions": { "items": [ { "user_id": 7, "milestone_key": "state_id",
      "completed_on": "2026-08-01", "registered_at": "2026-08-01T14:02:11Z" } ],
      "pagination": { "page": 1, "per_page": 100, "total": 1, "total_pages": 1 } },
    "submissions": { "items": [ { "user_id": 7, "milestone_key": "resume",
      "status": "approved", "submitted_at": "2026-07-28T09:15:00Z",
      "reviewed_at": "2026-07-29T11:40:00Z" } ],
      "pagination": { "page": 1, "per_page": 100, "total": 1, "total_pages": 1 } }
  },
  "meta": { "resource": "milestones", "version": "v1" }
}

Meetings

GET /meetings
GET /meetings/{id}

Milestone review and follow-up meetings. Filter the collection with ?participant_user_id= and/or ?status=scheduled|completed|canceled. Scope: meetings.read.

FieldTypeNotes
idinteger
participant_user_id / staff_user_idinteger
scheduled_at / completed_at / canceled_atstring or nullISO 8601 UTC.
duration_minutesinteger
meeting_typestringmilestone_review or followup.
statusstringscheduled, completed, or canceled.
notes / cancel_reasonstring or nullPlain text, not encrypted.
updated_atstringISO 8601 UTC.

Incremental sync

Add ?updated_since= (full ISO 8601, with an explicit offset or Z — e.g. 2026-08-28T00:00:00Z) to any collection to get only rows that changed at or after that instant, instead of a full pull every time. An unparseable value is silently ignored (no filter applied, same as an unrecognized query param elsewhere on this API) rather than an error — double-check the response if a sync looks larger than expected.

For milestone submissions specifically, "changed" means created or reviewed — a submission only ever moves once after creation, so this is exact for that resource, not an approximation.

Errors & rate limits

Every error is { "error": { "code", "message", "status" } }.

StatusCodeMeaning
401unauthorizedMissing, invalid, revoked, or expired key.
403ip_not_allowedKey is IP-restricted; this request's IP isn't on the list.
403insufficient_scopeKey doesn't carry a scope this resource needs.
404not_foundUnknown resource, or an id that doesn't exist in your org.
405method_not_allowedv1 is read-only — anything but GET lands here.
429rate_limited600 requests/minute per key. Check the Retry-After header (seconds) before retrying.
500internal_errorSomething broke on our end — safe to retry once, then contact support if it persists.

Python example

Nothing beyond requests is needed — a nightly incremental pull looks like:

import requests
from datetime import datetime, timezone

API_KEY = "fz_live_..."
BASE = "https://forzara.ai/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

def get_all(path, params=None):
    """Follows pagination until every page has been pulled."""
    params = dict(params or {})
    page, items = 1, []
    while True:
        params["page"] = page
        resp = requests.get(f"{BASE}/{path}", headers=headers, params=params)
        if resp.status_code == 429:
            raise RuntimeError(f"rate limited, retry after {resp.headers.get('Retry-After')}s")
        resp.raise_for_status()
        body = resp.json()["data"]
        items.extend(body["items"])
        if page >= body["pagination"]["total_pages"]:
            return items
        page += 1

since = datetime(2026, 8, 1, tzinfo=timezone.utc).isoformat()
roster = get_all("roster", {"updated_since": since})
print(f"{len(roster)} roster changes since {since}")