Skip to content

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 / InsomniaImportURLhttps://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.

All endpoints require a per-source registry API key as a bearer token:

Authorization: Bearer rgk_your_token_here

Keys 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.

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.

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, child Patient(s), and newborn Observations 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 a meta.profile naming 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).

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.

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.

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.

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.

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 (or timestamp) older than the version we already hold, the push is rejected with a conflict outcome 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 conflict rather 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.

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.

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"]
}
]
}
  • 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.json is the source of truth for the current contract.