Records API
Patient mapping
How Pierflow groups multi-page charts during capture, detects duplicate Patient rows, and maps Pierflow patient ids to your EMR's ids — end to end.
Real paper migration produces three problems Pierflow solves before records reach your EMR:
1. A patient's chart spans many pages. Page 1 has their name and MRN. Pages 2–47 are lab printouts and continuation sheets with no patient header. Without grouping, we'd emit 47 independent records, 38 of them orphans.
2. The same patient registers under two different spellings, or with a missing DOB, and ends up as two Patient rows.
3. Your EMR has its own patient ids. You need to look up Pierflow data using your id, not ours.
Three primitives address each in turn: chart folders (intra-batch grouping), duplicate detection (post-capture dedupe), and partner patient links (Pierflow id ↔ your id).
Chart folders#
A ChartFolder groups every page from one patient's chart so identity is resolved at folder level, not per page. The operator opens a chart, photographs N pages, then closes it. Every ProcessingJob carries the folder id; the resolver picks a Patient once the folder closes and every job is terminal.
Operator declarations
When opening a chart, the operator can declare:
Pick an existing patient — search by name or identifier. Sets declaredPatientId. Resolution short- circuits to DECLARED_BY_OPERATOR the moment the chart closes, regardless of extraction state. Late-arriving records auto- attach when their extraction lands.
Type an MRN — sets declaredMrn. We look up the MRN under the org's mrnSystem URI. If a Patient has it, resolution becomes MRN_LOOKUP.
Just start capturing — no declaration. Resolution runs purely on extracted evidence (name + DOB tuple, with any MRN we see on any page in the folder). Falls back to FUZZY_MATCH or NEW_PATIENT.
Telling the API about a chart
Programmatic ingest passes the same chartFolderId on every /v1/ingest/documents call for that chart. The folder is created server-side by the capture portal; pure-API partners can manage their own ids in the future, but for now use capture or the partner-side endpoint we ship next.
{
"organizationId": "org_lagoon_hospital",
"batchId": "bat_3xMA…",
"chartFolderId": "cf_8KqL…",
"source": {
"publicId": "pierflow/…/page_002",
"secureUrl": "https://res.cloudinary.com/…/page_002.png"
},
"documentType": "OUTPATIENT_CARD"
}Folder lifecycle
Folders have one of five effective states:
OPEN — operator is still capturing. New pages welcome.
EXTRACTING — closed; at least one job hasn't reached a terminal state yet. Resolution will trigger when it does.
RESOLVED — patient picked. Every record points at it.resolvedSource tells you which branch fired.
UNRESOLVED_NO_EVIDENCE — closed, all jobs terminal, but no patient block was extractable and no declaration was made. Surface a "Resolve now" affordance to your reviewer if you want to force a retry.
FAILED_NO_RESOLUTION — closed, every job failed extraction. Reviewer intervention required.
Duplicate detection#
A nightly cron walks every ACTIVE organisation and scores Patient pairs for likely duplication. Writes a PatientMergeCandidate row per pair; reviewers confirm in the staff portal's Merge queue tab.
How candidates are scored
Score 1.0 — MRN match. Same identifier value under the same MRN system URI. By definition, same person. Auto-flagged, never auto-merged.
Score 0.55–0.95 — Name + DOB. Token-set name ratio weighted at 0.65, DOB equality at 0.25, sex match at 0.1. We bucket candidates by DOB to keep the comparison tractable on large orgs.
Accepting a merge
Confirming a merge re-parents every ExtractedRecord,ChartFolder declaration, and PatientIdentifier from the candidate to the primary in one transaction. Identifier collisions are deduped. The candidate is soft-deleted via possibleDuplicateOfId so the next scoring pass ignores it.
Partner patient links#
A PartnerPatientLink maps a Pierflow Patient id to your EMR's patient id. Once a link exists, you can query Pierflow with your own id and the FHIR response carries it back as an Identifier so round-trips are self-describing.
Creating links
During cohort onboarding — POST /v1/partner-patient-links/bulk with up to 500 (mrn, external_id) pairs. We resolve each MRN under the org's mrnSystem. If we don't have a Patient for an MRN yet, we create a placeholder Patient + identifier + link (source PLACEHOLDER_FROM_MRN). When extraction later produces records carrying that MRN, the identifier already exists, so the records auto-attach to the same Patient — no re-linking.
{
"organization_id": "org_lagoon_hospital",
"external_system": "https://your-emr.example.com/patients/",
"items": [
{ "kind": "by_mrn", "mrn": "LH-00143-26", "external_id": "emr_8821" },
{ "kind": "by_mrn", "mrn": "LH-00200-26", "external_id": "emr_8822",
"placeholder_name": "Tunde Adeleke" },
{ "kind": "by_patient_id", "patient_id": "pat_b3f9c21a",
"external_id": "emr_8823" }
]
}On import acknowledgement — POST /v1/import-packages/:id/acknowledge with a patient_id_mappings array. Each pair becomes a PartnerPatientLink with source IMPORT_ACK. Use this when your EMR creates new patient ids during the import and you want to register them in the same call that confirms the import.
Querying by your own id
Once a link exists, this works:
curl https://www.pierflow.com/v1/organizations/org_lagoon_hospital/patients/by-external/emr_8821/fhir \
-H "Authorization: Bearer $PIERFLOW_KEY"We resolve the link internally and serve the merged FHIR Bundle. Your EMR doesn't need to maintain a mapping table.
FHIR Bundle with your id#
When a PartnerPatientLink exists between you and the requested Patient, the Bundle's Patient.identifier array carries three entries: Pierflow's internal id, the MRN under the org's mrnSystem, and your external_id under the URI you claimed at link time. use: secondary on the partner identifier.
{
"resourceType": "Patient",
"id": "pat_b3f9c21a",
"identifier": [
{ "system": "https://pierflow.com/patient", "value": "pat_b3f9c21a" },
{ "system": "https://healthos.ng/mrn/", "value": "LH-00143-26" },
{ "system": "https://your-emr.example.com/patients/",
"value": "emr_8821", "use": "secondary" }
],
"name": [
{ "use": "official", "text": "Adaeze Margaret Nwosu",
"family": "Nwosu", "given": ["Adaeze", "Margaret"] }
],
"gender": "female",
"birthDate": "1985-03-14"
}