Skip to content

API reference

Base URL: https://birthtracks.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://birthtracks.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://birthtracks.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

A practice mints its own keys in Birth Registry → Integrations. An EHR partner’s feed keys are issued by BirthTracks instead — see Two lanes. 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 key holds the same capabilities as a person, named by what it may do rather than who holds it: an ingest:write-only key is a Scribe key — it records births and reads nothing. Birth Registry → Integrations offers a one-click Scribe key preset that mints exactly that scope.

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.

The wire format is the same everywhere, but which kind of key a Bundle is sent under decides how it is processed. A key is either a practice’s own key (the native, identified lane) or a partner feed key bound to an EHR partner (the partner lane). The difference is a property of the key, not of anything in the Bundle.

Your own key (native lane) Partner feed key (partner lane)
How the key is obtained Minted by the practice in Birth Registry → Integrations. Minted by BirthTracks staff from your partner record — one key per practice, with the scopes you ask for — and handed over once. Every partner-key mint is recorded on BirthTracks’ audit trail. Ask your BirthTracks representative. A key minted in Integrations is always a native key, never a partner feed key.
Beta gate Not applied. Refused with 403 until your partner organisation is let into the partner-EHR beta, and whenever its contract status is not Active (Pending or Suspended).
What is stored The identified record, as sent. A de-identified spine only — masked at the door, before anything is persisted. See Partner onboarding.
Place of birth Required — a record without one is rejected (required, Encounter.location.location.type). Optional.
Provider attribution (attendant, certifier, other providers) Not recorded — Practitioner resources and the Encounter attendant are accepted but ignored. Recorded as provider identities keyed by your own provider id.

A partner that sends its feed under a key minted in Integrations is on the native lane: nothing is de-identified, a spine-only Bundle with no place of birth is rejected, and no provider attribution is recorded.

Requests are throttled at 120 per minute per API key, in one bucket shared by both endpoints (submissions and status polls draw on the same budget); over the limit returns 429 Too Many Requests. The budget keys on the API token, so each source key gets its own bucket rather than sharing one behind a partner’s egress IP; a request with no bearer token falls back to a per-IP bucket. Ingestion is asynchronous, so a steady poll for status (rather than a tight loop) stays well within budget.

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining headers, and a 429 adds Retry-After (seconds until the bucket refills) — back off for at least that long. The 429 is the one error that is not a FHIR OperationOutcome: it is the framework’s standard rate-limit response ({"message": "Too Many Attempts."} as JSON when your Accept header asks for JSON, otherwise a plain HTML page), so branch on the status code and Retry-After, not on the body.

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, or it is a partner feed key whose partner is not yet in the partner-EHR beta or whose contract status is not Active. Returns an OperationOutcome.
429 Too Many Requests Rate limit exceeded (see Rate limits). Not an OperationOutcome; honour Retry-After.

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 on a partner feed Provider attribution, so attendant-level benchmarking works — partner lane only; ignored on your own key. See below.
Encounter Recommended The attending provider (via participant, partner lane only), transfers of care, and the place of birth (via location) — see FHIR representation.
Location Yes on your own key The place of birth, referenced by the birth Encounter.location. Required on the native (identified) lane; optional for EHR partners.
Composition Optional A status of entered-in-error retracts the record. On a partner feed, the certifier is read from it.
Consent Optional The consent decision for the course of care. Only the first is read.

Provider attribution is recorded on the partner lane only (see Two lanes). On your own key, Practitioner resources are accepted without error but not stored, and no attendant or certifier is recorded. Everything below describes a partner feed.

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), physician (also obstetric/obstetrician, MD), 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. The midwife keywords are checked first, so a dual CNM, MD credential is recorded as a midwife.

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. Like all attribution, the attendant is recorded on a partner feed only.

An Encounter naming a recognised transfer type records a transfer of care instead; see the data dictionary.

On a partner feed, the certifier is the practitioner referenced by the first Composition.attester[].party, falling back to the first Composition.author when no attester names one. The referenced Practitioner is resolved to a provider identity exactly as above. On your own key no certifier is recorded.

The place of birth is read from the birth Encounter.location: resolve its Location.type coding against the NCHS birth-delivery-occurred value set. On your own (native) key a place of birth is required — a record with no resolvable birthplace is rejected with an OperationOutcome (required, Encounter.location.location.type), the same rule the CSV importer applies, because a roster birth is never landed on an unknown or defaulted site. EHR partners are exempt: partner-fed records are registry spine only, so place is optional there.

Location.type coding Place of birth
SNOMED 22232009 (Hospital) Hospital
SNOMED 91154008 (Free-standing birthing center) Birth center
SNOMED 408839006 (Planned home birth) · 67190003 (Home) Home
SNOMED 39350007 (Clinic / doctor’s office) Clinic
OTH (other) Other
No Location.type coding, and Location.physicalType = ve (vehicle) En route

An unrecognised Location.type coding is rejected naming the code — it is never silently mapped to “other”. The rejection is code value with expression Encounter.location.location.type. The vehicle fallback applies only when the Location carries no type coding at all: an unrecognised type code is rejected even if the Location is also a vehicle. The planned place of birth is read, when present, from the BFDR planned-birthplace Observation (same value set); it is optional and advisory.

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": {
"accepted": [
{ "reference": "CHART-001", "submission_uuid": "9b2f..." }
],
"rejected": []
},
"completedAt": "2026-05-01T12:00:00+00:00"
}

Each entry’s reference names the record it reports on: the course of care’s chart number (Bundle.identifier.value) when it has one, otherwise record[N] — its zero-based position in the submission (the inner Bundle’s position in batch mode).

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

A record that fails is landed in outcome.rejected[] under its own reference (chart number or record[N]) while the rest of the batch still lands. When the whole Bundle fails validation rather than an individual record, the ingestion still reaches completed — with accepted: 0 and a single rejected entry whose reference is the literal string "Bundle":

{
"status": "completed",
"accepted": 0,
"rejected": 1,
"outcome": {
"accepted": [],
"rejected": [
{ "reference": "Bundle", "outcome": { "resourceType": "OperationOutcome", "issue": [] } }
]
},
"completedAt": "2026-05-01T12:00:00+00:00"
}

status: "failed" is distinct from a completed ingestion that rejected every record. It means the push could not be processed at all, and outcome is a small error object rather than the accepted/rejected pair:

{
"id": "0c5e...",
"status": "failed",
"outcome": {
"error": "The bundle could not be processed and the push was not accepted.",
"exception": "App\\Exceptions\\…",
"correlation_id": "9f1c…"
},
"completedAt": "2026-05-01T12:00:00+00:00"
}
  • correlation_id is the support handle — quote it when you contact us, and it lets us find the failure without any record content in the ticket. (The exception message is deliberately not returned: a mapping error can embed submitted values, so only the exception class is surfaced.)
  • A related failure is a buffer-expired ingestion, whose outcome.error reads “The buffered Bundle for this ingestion is no longer available.” — the queued Bundle aged out of its short-lived buffer before the worker ran. Re-submit it.
Status Meaning
200 OK The ingestion receipt.
401 Unauthorized Missing, invalid, or revoked API key.
403 Forbidden The key lacks the ingest:read scope, or it is a partner feed key whose partner is not in the partner-EHR beta or not Active.
404 Not Found No ingestion with that id for the caller’s practice. An OperationOutcome with code not-found (“No ingestion found for that id.”).
429 Too Many Requests Rate limit exceeded. Not an OperationOutcome; honour Retry-After.

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": "CHART-001", "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": "CHART-001", "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. A retraction is also subject to the staleness rule above: one carrying a meta.lastUpdated (or timestamp) older than the version on file is rejected as a conflict (“Stale update ignored”) and the record stays live.

An accepted retraction is marked on the receipt with a retracted flag and the record’s version, so you can tell it from a create or an amendment:

{ "reference": "CHART-001", "submission_uuid": "0c5e…", "retracted": true, "version": 3 }

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 except a 429 rate-limit response (see Rate limits) 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.