Platform showcase — live registry figures, the node map and the provenance record
Documentation

Reference

API

The platform's own web interface is a client of this API, so anything the interface can do is available programmatically. Everything below is derived from the running service definition.

Base URL

Every endpoint lives under the /api/v1 prefix. There are two ways to reach it:

  • Local development: http://localhost:8001/api/v1 — the backend directly. The web application on port 3003 proxies its own /api/v1/* path to that backend, so both work in a browser.
  • Deployed environments: the base URL is supplied to the web application as configuration at build time and is not published here. Ask your study administrator for it.

Cross-origin requests are restricted by an allow-list. In a local checkout that is localhost:3000 through localhost:3003 and localhost:8001. Permitted request headers are Authorization, Content-Type, X-Study-ID, X-Request-ID and X-CSRF-Token; the response exposes X-Request-ID, the three X-RateLimit-* headers and Retry-After.

Collection routes are declared with a trailing slash. Request /api/v1/patients/, not /api/v1/patients: the latter redirects to the absolute URL, and most clients drop the Authorization header across a cross-origin redirect, which surfaces as a 401.

Authentication

Authentication is a two-leg flow ending in a bearer token. The first leg is always POST /auth/login with email and password, which returns one of four shapes; the second depends on which second factor your account has.

ResponseMeaningNext call
access_token + refresh_tokenNo second factor was owed. May carry mfa_setup_required: true, meaning policy expects you to enrol one.Use the token.
mfa_required: true + mfa_tokenThe account has an authenticator app enrolled.POST /auth/login/mfa with the token and the six-digit code.
otp_required: true + otp_token + otp_phone_hintNo authenticator app, but a verified phone: a one-time code has been sent by SMS. The hint is masked, never the full number.POST /auth/login/otp with the token and the code.
otp_setup_required: true + otp_tokenPassword verified but no second factor exists yet. The session is not authenticated until a code sent to a newly enrolled number is approved.POST /auth/login/otp/enroll with the token and a phone number, then POST /auth/login/otp.

SMS one-time codes are the fallback factor, never stacked on top of an authenticator app. Whether the SMS path is offered at all depends on deployment configuration.

Token lifetimes and account protection, as configured:

  • Access token: 60 minutes. Send it as Authorization: Bearer <token>.
  • Refresh token: 7 days. Exchange it at POST /auth/refresh for a fresh access token; POST /auth/logout ends the session and can also blacklist the refresh token.
  • MFA hand-off token: 5 minutes. SMS code token: 10 minutes, with at most 5 failed code checks before the token is blacklisted and you must start over, and a 30-second cooldown between code sends.
  • Repeated failed logins lock the account temporarily — checked before the password is verified, and answered with 423.

Browser clients also receive an HttpOnly session cookie on login, used by the web application's edge middleware; programmatic clients should ignore it and use the bearer token. GET /auth/me returns the authenticated user's profile, including their role and institution — what scope your token has.

Worked example

Login, complete the SMS second factor, then list your institution's patients. Replace the placeholders.

bashlogin → OTP → list patients
BASE=http://localhost:8001/api/v1

# 1. password leg
curl -s -X POST "$BASE/auth/login" \
  -H "Content-Type: application/json" \
  -H "X-Study-ID: tiger" \
  -d '{"email":"<you@example.org>","password":"<your-password>"}'
# → {"otp_required":true,
#    "otp_token":"<otp_token>",
#    "otp_phone_hint":"•••• 1234"}
#
# an authenticator-app account answers instead with:
# → {"mfa_required":true,"mfa_token":"<mfa_token>"}

# 2a. second factor — SMS code
curl -s -X POST "$BASE/auth/login/otp" \
  -H "Content-Type: application/json" \
  -H "X-Study-ID: tiger" \
  -d '{"otp_token":"<otp_token>","code":"<6-digit-code>"}'

# 2b. second factor — authenticator app
curl -s -X POST "$BASE/auth/login/mfa" \
  -H "Content-Type: application/json" \
  -H "X-Study-ID: tiger" \
  -d '{"mfa_token":"<mfa_token>","totp_code":"<6-digit-code>"}'
# both → {"access_token":"<jwt>","refresh_token":"<jwt>","token_type":"bearer"}

TOKEN=<jwt>

# 3. list patients (note the trailing slash)
curl -s "$BASE/patients/?page=1&page_size=25" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Study-ID: tiger"
# → {"patients":[…],"total":312,"page":1,"page_size":25}

# 4. refresh when the access token expires
curl -s -X POST "$BASE/auth/refresh" \
  -H "Content-Type: application/json" \
  -H "X-Study-ID: tiger" \
  -d '{"refresh_token":"<refresh_jwt>"}'

The patient list is scoped to your institution by the server. Filters are available for status, tumour type, surgical approach and a search across the study and subject identifiers.

The X-Study-ID header

Every request resolves to exactly one study: the X-Study-ID header, failing that a /tiger/… path prefix, failing that tiger by default.

This deployment serves one study. tiger is the only value that resolves; anything else is refused with a 400 naming it — from the database session rather than from any one endpoint, so from essentially every route that touches data. Send the header on every request anyway: the audit trail records a study on every row and the cutover write gate keys on it, and relying on the default makes the intent invisible in the audit trail.

Pagination

Collection endpoints take page and page_size and return the items alongside total, page and page_size. Pages are one-based, the default size is 25 and the maximum is 100; asking for more is a validation error rather than a silent clamp.

Four endpoints break that convention — check them rather than assuming:

  • /data/import/{job_id}/records — zero-based, default 50, maximum 200; the envelope adds review counters.
  • /data/history — one-based, default 20, maximum 100; one page number drives both its import and its export list, and the envelope has no total.
  • /admin/audit-log — one-based, default 50, maximum 200; items are under entries.
  • /admin/analytics/audit — zero-based, default 50, maximum 200; returns rows and total only.

Several administrative endpoints — migration, sync, background jobs, reconciliation — use limit and offset instead of pages.

There is no cursor pagination and no Link header. When paging a large collection under concurrent writes, prefer a filter that pins the window — a date range — over deep page numbers.

Errors

Errors use the standard FastAPI shape: a JSON object with a single detail key, and the HTTP status carrying the meaning.

jsonError shape
{ "detail": "Only study admins can export with PHI" }

Request-validation failures are the exception: a 422 returns detail as a list of objects, each naming the field location, a message and a type. Two other statuses also put a non-string there, so parse it defensively: a 429 adds a retry_after number beside it, and a 412 from the cutover write gate nests an object naming your institution's current state and the states the write would need.

StatusWhat it means here
400Malformed input the endpoint checks itself: an unsupported format, an empty file, a job not ready to download.
401Missing, expired or invalid bearer token; bad credentials.
403Authenticated but not permitted: wrong role, another institution's data, PHI without a study-admin role, or the cohort workbench while masquerading.
404No such record — or one you are not scoped to see.
409A precondition is unmet, such as promoting an import batch whose agreement has not been signed.
410An export job has passed its expiry.
412The cutover write gate: your institution is still in the parallel run against the legacy system, so this write is refused.
413Body too large: 50 MB on import and validation uploads, 10 MB on every other request body.
422Request-schema validation failed; see the list form above.
423Account temporarily locked after repeated failed logins.
429Rate limit exceeded; see below.

Rate limits

A sliding-window rate limiter runs in front of the API. The default allowance is 100 requests per minute, with tighter tiers on the endpoints worth abusing. Tiers are matched by path prefix, so sub-routes share their parent's allowance — /auth/login/mfa and /auth/login/otp draw on the same bucket as /auth/login:

EndpointLimit
/auth/login, /auth/register, /auth/register/mfa-verify10 / minute
/auth/register/mfa-setup, /auth/password-reset5 / minute
/patients/import — a configured tier with no route mounted at that path; imports use the data-exchange endpoints5 / minute
/patients/export, /analytics/export10 / minute
/analytics/public (unauthenticated aggregates)30 / minute
Everything else, including every /data/* data-exchange endpoint — none of them matches a tier prefix100 / minute

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. X-RateLimit-Reset and Retry-After are set only on a 429, both in seconds — back off on Retry-After rather than retrying immediately. The limiter counts by client IP, not by token, so everything behind one address shares a bucket.

OpenAPI schema

The service is FastAPI, so a complete machine-readable schema is generated from the same code that serves the requests.

  • Interactive documentation is development-only. The Swagger UI at /api/docs and the ReDoc rendering at /api/redoc are mounted only when the service environment is development; elsewhere both return 404.
  • The raw schema document is served at /openapi.json — at the service root, not under /api/v1.
bashFetch the schema locally
# interactive UI (development environment only)
open http://localhost:8001/api/docs

# the schema itself
curl -s http://localhost:8001/openapi.json | jq '.paths | keys | length'

Endpoints are grouped by tag — Authentication, Users, Institutions, Patients / eCRF, Analytics, Cohort Comparison, Dashboard, Notifications, Announcements, Agent Engine, Data Exchange, Content Pages, Committee Reviews, and a set of administration groups. The guides here cover the common ones: import, export, templates, validation and history and analytics.

Scope applies to the API, not just the interface

Institution scoping, role gates, small-cell suppression and the cutover write gate are all enforced in the API layer. A token can do exactly what its holder can do in the web interface — no more. Everything an API call does is audited the same way.