API reference
Base URL: https://laravel-sandbox.twosportday.com
The full machine-readable contract is published as an OpenAPI 3.1 spec at
/openapi.json. It is the
source of truth for the endpoints below — rather than maintain a separate
collection that can drift, import the spec directly:
- Postman / Insomnia — Import → URL →
https://laravel-sandbox.twosportday.com/openapi.json; both build a ready-to-run request collection from it. - Generate a client — point any OpenAPI generator at the spec, e.g.
openapi-generator-cli generate -i https://laravel-sandbox.twosportday.com/openapi.json -g python.
Authentication
Section titled “Authentication”All endpoints require a per-source registry API key as a bearer token:
Authorization: Bearer rgk_your_token_hereKeys are minted per practice in Registry → Settings. A key is shown once at
mint time, prefixed rgk_; only a hash is stored, so a leaked database can never
recover a usable credential. Keys do not expire — they stay valid until
revoked.
Each key carries scopes that gate which endpoints it may call:
| Scope | Grants |
|---|---|
ingest:write |
Submit Bundles — POST /api/registry/fhir/Bundle. |
ingest:read |
Read ingestion status — GET /api/registry/fhir/ingestions/{id}. |
A missing, invalid, or revoked key returns 401 Unauthorized; a valid key lacking
the scope a route requires returns 403 Forbidden. Both are FHIR
OperationOutcomes. See Getting started.
Rate limits
Section titled “Rate limits”Each endpoint is throttled at 120 requests per minute; over the limit returns
429 Too Many Requests. Ingestion is asynchronous, so a steady poll for status
(rather than a tight loop) stays well within budget.
POST /api/registry/fhir/Bundle
Section titled “POST /api/registry/fhir/Bundle”Submit a FHIR R4 BFDR Bundle for asynchronous ingestion. The body must be a JSON
object with resourceType: "Bundle". Send Content-Type: application/fhir+json.
Requires the ingest:write scope.
One or many courses of care. A Bundle is either:
- A single course of care — the Bundle holds the mother
Patient, childPatient(s), and newbornObservations directly. One record. - A batch — the Bundle’s entries are themselves Bundles; each inner Bundle is one course of care. Non-Bundle entries are ignored in this mode.
Each course of care is mapped and validated independently, so a batch lands its
good records and reports the bad ones individually via the accepted/rejected
counts on the status receipt.
What makes a record valid. A course of care is accepted only if it has:
- exactly one mother
Patient(resolved from ameta.profilenaming a mother/maternal BFDR profile), and - at least one child
Patient(profile naming child/newborn/fetus/decedent).
A record with no mother, more than one mother, or no child is rejected —
landed in outcome.rejected with an OperationOutcome explaining why — while the
rest of the batch still lands. Records are checked for this structural shape, not
yet for full BFDR IG conformance (see Limitations).
Idempotent retries. Send an Idempotency-Key header to make a network retry
of a POST safe. The key is scoped to your practice and applies to the whole
request (the Bundle). The first POST with a given key creates the ingestion; any
later POST with the same key returns the original 202 Accepted and its
Content-Location without re-ingesting — so a retry after a dropped response
never doubles a birth. Use a fresh key per distinct submission; reusing a key for
a different Bundle replays the first one. The header is optional — a request
without it is processed as before (see Duplicate handling
for how same-birth records are flagged when no key is used).
Responses
| Status | Meaning |
|---|---|
202 Accepted |
Queued. The Content-Location header holds the status URL; the body is a FHIR OperationOutcome. |
400 Bad Request |
The body was not a JSON FHIR Bundle. Returns an OperationOutcome. |
401 Unauthorized |
Missing, invalid, or revoked API key. Returns an OperationOutcome. |
403 Forbidden |
The key lacks the ingest:write scope. Returns an OperationOutcome. |
429 Too Many Requests |
Rate limit exceeded (see Rate limits). |
Resources in a course of care
Section titled “Resources in a course of care”Beyond the mother and child Patients that make a record valid, these resources
are read when present. Anything else in the Bundle is ignored.
| Resource | Required | What it contributes |
|---|---|---|
Patient (mother) |
Yes, exactly one | Maternal demographics. Role resolved from a BFDR mother/maternal meta.profile. |
Patient (child) |
Yes, one or more | The newborn(s). Role resolved from a child/newborn/fetus/decedent profile. |
Observation |
Recommended | Birth weight, gestational age, and Apgar scores — see the LOINC codes. |
Practitioner |
Recommended | Provider attribution, so attendant-level benchmarking works. See below. |
Encounter |
Recommended | The attending provider (via participant), and transfers of care — see FHIR representation. |
Composition |
Optional | A status of entered-in-error retracts the record. |
Consent |
Optional | The consent decision for the course of care. Only the first is read. |
Practitioner — provider attribution
Section titled “Practitioner — provider attribution”A provider’s stable identity is the first non-NPI identifier.value, falling
back to the resource id. The NPI is read when present but is kept as a discovery
signal only and is never the key — doulas and CPMs frequently have none. A
Practitioner carrying no stable id is skipped rather than stored under a
synthesized key a later push would not match, so send a stable id and keep it
stable.
Role is read from the practitioner’s qualification text/display and matched
coarsely by keyword — midwife (also CNM, CPM, LM), doula, nurse (also
RN), assistant; anything unrecognised falls to other, which is excluded from
attendant-type benchmarks so an unmatched credential can never over-count
attendants.
Unlike the mother and child, a provider’s display name and credentials are retained — provider attribution is not patient PHI.
Encounter — the attending provider
Section titled “Encounter — the attending provider”The attendant is the Encounter.participant marked with the HL7 v3
ParticipationType ATND (“attender”), matched independent of the coding system.
Nothing else promotes a practitioner to attendant: a doula listed as a participant
is recorded as present, not as attending.
An Encounter naming a recognised transfer type records a transfer of care
instead; see the data dictionary.
GET /api/registry/fhir/ingestions/{ingestion}
Section titled “GET /api/registry/fhir/ingestions/{ingestion}”Get the status of a submitted Bundle, scoped to the practice that owns the key.
Requires the ingest:read scope. Poll until status is completed or failed.
Path parameters
| Name | Description |
|---|---|
ingestion |
The ingestion id from the submit response’s Content-Location header. |
Response body
{ "id": "0c5e...", "status": "completed", "accepted": 1, "rejected": 0, "outcome": null, "completedAt": "2026-05-01T12:00:00+00:00"}status is one of queued, processing, completed, failed. On a completed
ingestion outcome is an object with accepted and rejected arrays of
per-record detail — each rejected entry carries a FHIR OperationOutcome, and
each accepted entry can carry a suspected_duplicate flag (see
Duplicate handling).
| Status | Meaning |
|---|---|
200 OK |
The ingestion receipt. |
401 Unauthorized |
Missing, invalid, or revoked API key. |
403 Forbidden |
The key lacks the ingest:read scope. |
404 Not Found |
No ingestion with that id for the caller’s practice. |
429 Too Many Requests |
Rate limit exceeded. |
Duplicate handling
Section titled “Duplicate handling”Ingestion is accept-and-flag: a record that looks like one already on file is still stored, then marked for review — never silently merged or dropped.
The registry fingerprints each course of care by the mother’s name and the
newborn date/time, scoped to your practice (stored only as a keyed hash, never
in the clear). When a later submission matches a record already on file — the same
birth arriving again, including from a different input path such as a CSV import —
it is accepted and stored, then flagged in the ingestion outcome:
{ "status": "completed", "accepted": 1, "rejected": 0, "outcome": { "accepted": [ { "reference": "Bundle.entry[0]", "submission_uuid": "0c5e…", "suspected_duplicate": true } ], "rejected": [] }, "completedAt": "2026-05-01T12:00:00+00:00"}- Matching tolerates formatting differences: the mother’s name is compared case- and whitespace-insensitively, and the birth time is normalized.
- If a record is missing the mother’s name or the newborn date/time, no duplicate check runs for it.
- A previously voided record is ignored, so a birth can be re-submitted after an earlier entry is voided.
- Resolving a suspected duplicate (merge, void, or keep) is a review step; the API does not do it automatically.
Chart numbers. Bundle.identifier.value is your record id
(external_record_id) and is unique within your practice (it may repeat
across different practices). Use one stable chart number per course of care and
re-send under that same id rather than minting a new one — a re-send under the
same id amends the record, while a new id lands a
second, flagged record.
Amendments and retractions
Section titled “Amendments and retractions”Sending a Bundle whose identifier.value matches a record already on file for
your practice amends that record: the correction is applied, the record’s
version advances, and its previous values are kept as history. Nothing is
duplicated, and the receipt marks the record so you can tell a correction from a
create:
{ "reference": "Bundle.entry[0]", "submission_uuid": "0c5e…", "amended": true, "version": 2 }Two rules apply to amendments and not to first submissions:
- Older updates are ignored. If the Bundle carries a
meta.lastUpdated(ortimestamp) older than the version we already hold, the push is rejected with aconflictoutcome reading “Stale update ignored” and the record is left alone. Send your source system’s own last-modified stamp so retries and out-of-order deliveries can’t roll a record back. - An amendment may not merge two records. If a correction would make the
record a duplicate of a different record (same mother + newborn date/time), it
is rejected as a
conflictrather than merged. Resolve the two records first.
Retractions. To withdraw a record, send its chart number with either a
Composition whose status is entered-in-error, or a transaction/batch entry
with "request": { "method": "DELETE" }. The record is voided and drops out of
statistics and benchmarks immediately. A retraction identifies the record by chart
number only, so it does not need to carry the full course of care; retracting an
unknown chart number is rejected as not-found, and retracting twice is a no-op.
Re-sending a record after retracting it revives it as a further amendment version — one record, with its history intact, rather than a second one under the same id.
Idempotent retries vs. duplicate flagging are different mechanisms. An
Idempotency-Key makes a retry of the same POST
a no-op that replays the original receipt. Duplicate flagging catches the same
birth arriving again — a later submission (no key, a new chart number, or a
different input path such as CSV) whose mother/newborn fingerprint matches a
record already on file. The first prevents accidental re-ingestion; the second
surfaces genuine duplicates for review.
Consent
Section titled “Consent”Include a FHIR Consent resource in the Bundle to record the patient’s consent
decision for the course of care. The registry reads the first Consent and treats
it as granted only when its status is active and it carries no deny
provision; anything else — proposed, rejected, inactive, an explicit deny,
or no Consent at all — is recorded as not granted.
Consent is recorded, not enforced: a missing or denied consent does not block
ingestion. A consent decision is stamped on every course of care at intake (the
Consent.dateTime is preserved when present), so the registry never treats
unconsented data as consented. How unconsented records may be used downstream is a
policy decision, separate from intake.
Error shape
Section titled “Error shape”Every error is a FHIR OperationOutcome:
{ "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "structure", "diagnostics": "Request body must be a JSON FHIR Bundle (resourceType \"Bundle\").", "expression": ["Bundle.resourceType"] } ]}Limitations and versioning
Section titled “Limitations and versioning”- No list endpoint. The API is push-and-poll: submit a Bundle, then read that ingestion by id. There is no endpoint to list or query prior ingestions or records, so keep the ingestion ids — and your own chart numbers — that you care about.
- Structural validation only. Records are validated for the BFDR shape (recognized roles, exactly one mother, at least one child) and the declared IG version is stored, but Bundles are not yet checked for full BFDR Implementation Guide conformance.
- Unversioned, additive. Endpoints live under
/api/registry/fhir/with no version segment. Changes are additive where possible; the OpenAPI spec at/openapi.jsonis the source of truth for the current contract.