LOSPOR API
The canonical V7 API prefix is /v1. A running API exposes:
/health/livefor process health/health/readyfor process and database readiness/v1/capabilitiesfor client compatibility and feature discovery/openapi.jsonfor the generated endpoint contract
Native clients authenticate with:
Authorization: Bearer <token>
X-LOSPOR-Client: mobile
X-LOSPOR-Client-Version: <version>
Browser clients use an API-owned HttpOnly session cookie. Revision, idempotency, source, and lock headers are fully described in OpenAPI and remain part of the shared Core sync protocol.
The v7.3.0 OpenAPI document explicitly describes every supported client, administrator, maintenance, and health operation: parameters, request bodies, response bodies, downloads, headers, authentication, and endpoint errors. Generation fails if a route lacks a contract or a stale contract lacks a route. The two secret scheduled jobs are kept in a separate internal inventory and are not exposed for SDK generation.
The API remains first-party only. A complete transport contract is not a promise of unrestricted access. Third-party applications still require a later client-registration, scopes, rate-limit, and support-policy release.
Case editing leases
Lock acquisition and heartbeat use one PostgreSQL compare-and-set statement. Only an expired lease or the same user/device may update the row. If lock transport itself is unavailable, clients deliberately remain editable so clinical work can continue; monotonic section revisions still prevent a stale save from silently replacing newer server data.
Clinical write serialization
Finalization and every authoritative section/event write lock the same parent case row inside a direct PostgreSQL transaction. Once finalization has that lock, a concurrent write waits and then fails after the case becomes complete; if the write got the lock first, finalization waits and snapshots its result. Database triggers enforce the completed-case guard even if application code is bypassed.
Each case has monotonic clinical, event, and relational revisions in addition to section revisions. Research manifest v2 stores all of them, so a child-row change cannot pass export snapshot validation merely because a parent timestamp was unchanged.
A save built on a stale revision is refused with 409 and the server's current
version of the section. A client that means to overwrite it anyway — a queued
offline save is stale by definition — sends overrideConflict in the body, or
the x-lospor-override-conflict header. That is recorded: an audit entry names
the sections overwritten, the revision the client believed it held, and the
revision it discarded. Setting the flag when there was no conflict records
nothing.
Finalization appends rather than overwrites. Each finalization of a case is its own record, carrying who performed it and, for a correction, why and which record it supersedes. A database trigger rejects UPDATE and DELETE, so a case that was finalized, unfinalized, corrected and finalized again retains what was first attested to.
Server time and the last change made
Every response carries X-LOSPOR-Server-Time: the server's clock in epoch
milliseconds, exposed to browsers through CORS. Clients use it to correct
"now" on the intraoperative timeline; it is never applied to an entered time.
Timetable event writes (POST /v1/cases/{id}/events, PUT and DELETE /v1/cases/{id}/events/{eventId}) accept X-Lospor-Made-At, when the change
was made on the device. It is capped at two minutes past the server's now. A
write made before the entry's latest change is refused with 412 and
{ "code": "SUPERSEDED" }: the last change made wins across devices, not the
last to arrive. Writes without the header count as made when they arrive.
Events written before 9.13.0 carry no time and never refuse.
Events may carry recordedAt (when the entry was made, an ISO instant with
offset) and stopConfirmed. A stop dated more than a minute after it was
entered is asked about when its time comes; finalization refuses a case with
such a stop unanswered (unconfirmed_stops).
Export completeness
OMOP batches above 5000 matching cases return HTTP 422 with
EXPORT_LIMIT_EXCEEDED and no partial export. Personal account export is a
streamed ZIP with a manifest and cursor-paged NDJSON records.
Governed Research Browser exports are background jobs. Their creation record accept only finalized-case cohorts. Their creation record freezes the normalized cohort definition, action-specific institution scope, database cutoff, source version, and exact parent, event, relational, and section revisions. Revision capture uses a repeatable-read transaction and stores a manifest hash. If a captured case changes before artifact generation, the job fails visibly and must be recreated instead of silently omitting that case. A completed job stores one immutable checksummed artifact; later downloads stream that artifact instead of querying the live database again. OMOP CSV is a ZIP containing a manifest and separate table CSV files. OMOP pages are mapped once and written through private working objects rather than repeatedly loading the cohort.
Use RESEARCH_EXPORT_STORAGE_DRIVER=filesystem only for local or self-hosted
private storage. Vercel requires RESEARCH_EXPORT_STORAGE_DRIVER=s3 plus a
private S3-compatible bucket. Configure an object-storage lifecycle rule to
abort stale incomplete multipart uploads. The internal export worker isolates
individual job failures and supports bounded batch recovery through the
scheduled /v1/internal/research-exports/process route.