API

Records API endpoints

Every /v1 endpoint partners can call, with auth notes and example request shapes. Reach for the Postman collection (downloadable from your /portal/overview) for runnable versions.

Every request needs Authorization: Bearer pf_test_sk_… (or pf_live_sk_…). Issue keys from /portal/keys. Keys are server-side only — never embed them in mobile or browser code.

Organizations#

Each request acts on a customer organization your key is linked to via a PartnerOrganizationLink. Use these to list and inspect the orgs available to you.

Method · PathPurpose
GET /v1/organizationsList orgs this key may act on
GET /v1/organizations/:orgIdOne organization, including mrnSystem
GET /v1/organizations
HTTP
curl -H "Authorization: Bearer $PIERFLOW_KEY" \
  https://www.pierflow.com/v1/organizations

Capture and ingest#

Use these to push paper records through extraction. Image bytes go direct to Cloudinary using a signed URL — Pierflow never proxies files.

Method · PathPurpose
POST /v1/uploads/signGet a one-shot Cloudinary upload signature
POST /v1/scan-batchesCreate a batch (groups pages from one session)
POST /v1/ingest/documentsRegister an uploaded asset as a ProcessingJob
GET /v1/ingest/jobs/:jobIdPoll ProcessingJob state + extracted records

Typical ingest flow

curl
bash
# 1. Create a batch
curl -X POST https://www.pierflow.com/v1/scan-batches \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "org_lagoon_hospital",
    "label": "Ward A migration · cohort 1",
    "priority": "NORMAL"
  }'

# 2. Sign a Cloudinary upload
curl -X POST https://www.pierflow.com/v1/uploads/sign \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "org_lagoon_hospital",
    "batchId": "bat_3xMA…"
  }'

# 3. Upload directly to Cloudinary
# (multipart-form upload using the fields returned in step 2)

# 4. Tell Pierflow about the asset
curl -X POST https://www.pierflow.com/v1/ingest/documents \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "org_lagoon_hospital",
    "batchId": "bat_3xMA…",
    "source": {
      "publicId": "pierflow/org_…/page_001",
      "secureUrl": "https://res.cloudinary.com/…/page_001.png"
    },
    "documentType": "OUTPATIENT_CARD"
  }'

# 5. Poll
curl https://www.pierflow.com/v1/ingest/jobs/job_4HD… \
  -H "Authorization: Bearer $PIERFLOW_KEY"
Chart folders. If you're photographing multi-page charts, group pages by passing the same chartFolderId on every /v1/ingest/documents call for that chart. Identity is resolved at folder level — see Patient mapping.

Patients#

After extraction, validated records roll up into a per-patient FHIR Bundle.

Method · PathPurpose
GET /v1/organizations/:orgId/patientsList patients for an org
GET /v1/organizations/:orgId/patients/:patientId/fhirMerged FHIR R4 Bundle
GET /v1/organizations/:orgId/patients/by-external/:externalId/fhirBundle by your own EMR id (via PartnerPatientLink)

Both FHIR endpoints accept the same query params: include (comma-separated resource types), date_from, date_to (YYYY-MM-DD filters on Encounter.period.start). When a PartnerPatientLink exists, the Patient.identifier array in the response includes your external_id as a secondary identifier so your EMR can round-trip without out-of-band state.

Map Pierflow Patient ids to your EMR's patient ids in one place. Cohort imports go through /bulk; per-patient mappings from acknowledge calls flow in automatically (see Acknowledge). Once linked, you can query Pierflow with your own id via the by-external FHIR endpoint above.

Method · PathPurpose
POST /v1/partner-patient-linksCreate or update one mapping (by MRN or patient id)
POST /v1/partner-patient-links/bulkUp to 500 mappings in one call
GET /v1/partner-patient-links?external_id=…Lookup by your id
GET /v1/partner-patient-links?patient_id=…Lookup by Pierflow id
POST /v1/partner-patient-links (by MRN)
HTTP
curl -X POST https://www.pierflow.com/v1/partner-patient-links \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "by_mrn",
    "organization_id": "org_lagoon_hospital",
    "mrn": "LH-00143-26",
    "external_id": "emr_patient_8821",
    "external_system": "https://your-emr.example.com/patients/",
    "placeholder_name": "Adaeze Margaret Nwosu"
  }'

If no Patient exists under that MRN yet, we create a placeholder Patient + identifier + link with source PLACEHOLDER_FROM_MRN. Future extracted records carrying that MRN auto-attach to the same Patient — the link survives the switch.

Import packages#

Nightly ZIP of validated records per (partner, org) tuple. Download once, acknowledge once.

Method · PathPurpose
GET /v1/organizations/:orgId/import-packagesList packages for an org (status, counts, expiresAt)
GET /v1/import-packages/:packageId/downloadShort-lived signed Cloudinary URL for the ZIP
POST /v1/import-packages/:packageId/acknowledgeConfirm import + register patient_id_mappings
POST /v1/import-packages/:packageId/acknowledge
HTTP
curl -X POST https://www.pierflow.com/v1/import-packages/pkg_4HD…/acknowledge \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "imported_patient_count": 47,
    "failed_patient_ids": [],
    "partner_import_reference": "emr-import-2026-06-06-001",
    "patient_id_mappings": [
      {
        "pierflow_patient_id": "pat_b3f9c21a",
        "external_id": "emr_8821",
        "external_system": "https://your-emr.example.com/patients/"
      }
    ]
  }'

patient_id_mappings is optional. When present, each pair becomes a PartnerPatientLink with source IMPORT_ACK. Per-item outcomes return in the response so you retry only the failures.

Webhooks#

Subscribe to events so you don't have to poll. Endpoints are signed with HMAC-SHA256; verify X-Pierflow-Signature before trusting any payload.

EventFired when
processing_job.completedExtraction finished — record is AUTO_APPROVED or AWAITING_REVIEW
processing_job.failedExtraction errored; ProcessingJob.status = FAILED
import_package.readyA new ImportPackage moved to READY
test.pingSent on demand from /portal/webhooks

Manage endpoints from your /portal/webhooks page. Synchronous delivery with one retry after 30s today; persistent delivery audit ships next.