FlexpaFlexpa
Developer PortalGet a DemoTry it yourself

Guides

  • Home
  • Quickstart
  • Agent guide
  • Claims data guide
  • Financial data guide
  • Parsing FHIR data

Network

  • Network guide
  • Endpoint directory
  • CHPL directory
  • Directory MCP server

Consent

  • OAuth
  • Patient linking
  • Usage patterns
  • Patient access

Records

  • FHIR API
  • Webhooks
  • DestinationsNew
    • Security model
    • Delivered objects
    • Auditing deliveries
    • Verify a delivery
  • Data Sheet
  • Node SDK
  • SMART Health Links API
  • Terminology
  • Claims to clinical

Misc

  • ChangelogNew
  • Support
  • Flexpa OS
  • We're hiring

Destinations

Forward FHIR data to your own cloud storage after every successful sync. With a destination configured, Flexpa writes each patient's synced resources to your Amazon S3 bucket — so you can build a data lake, run analytics, or load records into your own systems without polling the FHIR API.

Today destinations support Amazon S3. Delivery is push-based: there's nothing to poll, and writes happen automatically as part of each sync.

#How it works

  1. You create an IAM role in your AWS account that lets Flexpa write to a bucket you own.
  2. After every successful sync, Flexpa assumes that role and writes the patient's resources to your bucket as NDJSON.
  3. Each delivery is recorded so you can audit what was written, and when.

#Setup

Configure destinations in Portal under Destinations.

Destinations are configured separately for test and live modes. A sync only delivers to a destination whose mode matches the authorization.

  1. Create the S3 bucket in your AWS account — Flexpa never creates it.
  2. Create an IAM role named exactly flexpa-data-delivery (the name is required — any other name is accepted in Portal but fails at delivery), with the trust policy on the right and write access to the bucket.
  3. In Portal, add a destination with the bucket name, region, and the role ARN. Flexpa generates a unique external ID and shows it once.
  4. Click Test connection to verify Flexpa can assume the role and write to the bucket.
  5. New destinations are enabled by default, so the next successful sync delivers to your bucket automatically — there's no toggle to flip to start delivery. Use the enabled toggle in Portal to pause or resume delivery for an existing destination.

The external ID is shown only once, when you create the destination. Copy it into your trust policy's sts:ExternalId condition before closing the dialog. If you lose it, delete the destination and create a new one.

IAM setup

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::919310032435:role/flexpa-customer-data-delivery"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "<external-id-from-portal>"
        }
      }
    }
  ]
}

The exact Flexpa role ARN and your external ID are shown in Portal when you create the destination — copy them from there rather than from this page.


#Security model

Flexpa never stores long-lived credentials for your AWS account. Each delivery uses a short-lived, cross-account AWS STS AssumeRole into a role you control and can revoke at any time.

  • Your role must be named exactly flexpa-data-delivery, and its trust policy names a single stable Flexpa role (flexpa-customer-data-delivery), not Flexpa's whole account, so you grant the narrowest possible trust. Flexpa's delivery role is only permitted to assume a role with this exact name.
  • A unique external ID, generated per destination and shown once at creation, must appear in your trust policy's condition. This mitigates the confused-deputy problem: even if someone learned the Flexpa role ARN, they couldn't make Flexpa write to your bucket without your external ID.
  • You scope the role's permissions to exactly the bucket and flexpa/ prefix you choose.

#Encryption

Objects are written with server-side encryption. By default Flexpa uses SSE-S3 (AES256). To use your own KMS key, provide its ARN when creating the destination and grant the role kms:GenerateDataKey and kms:DescribeKey on that key (see the policy on the right) — both the role's IAM policy and the key's resource policy must allow it.

KMS key permissions (optional)

{
  "Effect": "Allow",
  "Action": ["kms:GenerateDataKey", "kms:DescribeKey"],
  "Resource": "arn:aws:kms:REGION:ACCOUNT_ID:key/KEY_ID"
}

#Delivered objects

After each successful sync, Flexpa writes a single newline-delimited JSON (NDJSON) object containing the FHIR resources loaded by that sync — one resource per line. The initial connection delivers the patient's full record (equivalent to $everything); each subsequent refresh for MULTIPLE usage authorizations delivers only the resources that changed since the previous sync, so a refresh object is incremental rather than the cumulative set $everything returns. Both include resources derived from claims. Objects are written with content type application/fhir+ndjson.

#Object key layout

Keys follow a fixed, predictable layout so you can build downstream tooling against it:

flexpa/{externalId}/{consentId}/{patientAuthorizationId}/{timestamp}-{syncJobId}.ndjson

Key segments

externalId

Your application's user identifier, passed through the Consent SDK. This is the primary way to join delivered objects back to your own users.

consentId

The consent ID for the session. A consent may produce multiple patient authorizations.

patientAuthorizationId

The patient authorization that produced this delivery.

timestamp

ISO-8601 UTC timestamp of the delivery, with colons and dots normalized to -.

syncJobId

The sync job that produced the data.

Every successful sync writes a new object — the initial connection and each subsequent refresh for MULTIPLE usage authorizations. Objects are never overwritten; the timestamp and syncJobId keep each key unique. Pair destinations with webhooks to be notified the moment a sync completes.

Delivered object

{"resourceType":"Patient","id":"...","name":[]}
{"resourceType":"Coverage","id":"...","status":"active"}
{"resourceType":"ExplanationOfBenefit","id":"...","status":"active"}
{"resourceType":"Encounter","id":"...","class":{}}

Reading NDJSON

import { createInterface } from 'node:readline';

// `stream` is the S3 object body
for await (const line of createInterface({ input: stream })) {
  if (!line) continue;
  const resource = JSON.parse(line);
  // handle resource by resource.resourceType
}

#Auditing deliveries

Every delivery is recorded, and you can audit what was written — resource counts by type, the object size, and the object key — out-of-band via the REST API, without reading the objects themselves. Both endpoints require an Application Access Token.

GEThttps://api.flexpa.com/rest/deliveries

#List deliveries

List delivery attempts for your application's destinations with cursor-based pagination, newest first. Failed attempts are included, so you can also use this endpoint to monitor delivery health.

Query parameters

limitnumber

Maximum number of deliveries per page. Defaults to 20, maximum 100.

cursorstring

Pagination cursor from a previous response's meta.nextCursor. Omit for the first page.

consentstring

Filter to deliveries produced by a single consent.

patient_authorizationstring

Filter to deliveries produced by a single patient authorization.

statusstring

Filter by delivery status: SUCCEEDED or FAILED.

sincestring

Only deliveries completed at or after this ISO 8601 timestamp.

Response fields

dataarray

An array of delivery records.

idstring

The unique identifier for the delivery record.

statusstring

SUCCEEDED or FAILED.

modestring

The destination's operational mode, TEST or LIVE. Always matches the mode of the token.

bucketstring

The destination bucket the object was written to.

keystring

The object key within the bucket.

bytesnumber | null

Size of the delivered object in bytes.

completedAtstring | null

When the delivery finished (ISO 8601).

errorMessagestring | null

For FAILED deliveries, the AWS error Flexpa received writing to your bucket (for example an AccessDenied from your role or bucket policy). null on successful deliveries.

createdAtstring

When the delivery was recorded (ISO 8601).

syncJobIdstring

The sync job that produced the data.

patientAuthorizationIdstring | null

The patient authorization that produced this delivery.

metaobject

Pagination metadata: hasMore and nextCursor.

Request

GET
/rest/deliveries
ACCESS_TOKEN=your-application-access-token

curl "https://api.flexpa.com/rest/deliveries?status=SUCCEEDED&since=2026-08-01T00:00:00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "data": [
    {
      "id": "01234567-89ab-cdef-0123-456789abcdef",
      "status": "SUCCEEDED",
      "mode": "LIVE",
      "bucket": "your-bucket",
      "key": "flexpa/user-123/.../2026-08-12T01-02-03-000Z-job.ndjson",
      "bytes": 123456,
      "completedAt": "2026-08-12T01:02:03.000Z",
      "createdAt": "2026-08-12T01:02:03.000Z",
      "syncJobId": "job-id",
      "patientAuthorizationId": "01234567-89ab-cdef-0123-456789abcdef",
      "errorMessage": null
    }
  ],
  "meta": { "hasMore": false, "nextCursor": null }
}

GEThttps://api.flexpa.com/rest/deliveries/:id/manifest

#Delivery manifest

Retrieve an audit manifest for a single delivery, shaped like a FHIR Bulk Data export manifest. Returns 404 if the delivery does not exist, belongs to another application, or belongs to the other mode of your application.

One deliberate deviation from the Bulk Data IG: Flexpa delivers a single mixed-type NDJSON object per sync, not one file per resource type. The manifest therefore contains one output entry per resource type present in the delivery, and every entry points at the same S3 object — the count tells you how many lines of that type the object contains.

Manifest fields

transactionTimestring

When the delivery completed (ISO 8601).

requeststring

The canonical URL of this manifest.

requiresAccessTokenboolean

Always false — the output URLs are objects in your own bucket, read with your own AWS credentials.

outputarray

One entry per resource type: type, url (the s3://bucket/key of the delivered object — the same object for every entry), and count (lines of that type). Empty for deliveries recorded before manifests existed.

errorarray

Always empty. Flexpa records failures on the delivery record itself (status: FAILED on the list endpoint), not as Bulk Data error entries — a failed delivery writes no object, so there is no error file to reference.

extensionobject

Flexpa-specific audit context, namespaced under the single key https://flexpa.com/docs/records/destinations#delivery-manifest (this section's URL): status (SUCCEEDED or FAILED — a FAILED delivery wrote no object, so its output is empty), patientAuthorizationId, consentId, appExternalUserId (your flexpa_external_id for the user), syncJobId, bytes, and the checksum fields below.

extension.checksumSha256string | null

Base64 SHA-256 digest S3 computed and stored for the object. For multipart deliveries this is a COMPOSITE digest (see checksumType). null for deliveries recorded before checksums existed.

extension.checksumTypestring | null

COMPOSITE (the SHA-256 of the concatenated raw per-part digests — the S3 multipart convention) or FULL_OBJECT (the plain SHA-256 of the object bytes, used when the delivery fit in a single part). null whenever checksumSha256 is null.

extension.partSizeBytesnumber | null

The exact part size the upload used. You need this to reproduce a COMPOSITE digest from the object bytes. null whenever checksumSha256 is null.

extension.partCountnumber | null

Number of parts behind a COMPOSITE digest. null for FULL_OBJECT, and whenever checksumSha256 is null.

Request

GET
/rest/deliveries/:id/manifest
ACCESS_TOKEN=your-application-access-token

curl "https://api.flexpa.com/rest/deliveries/$DELIVERY_ID/manifest" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "transactionTime": "2026-08-12T01:02:03.000Z",
  "request": "https://api.flexpa.com/rest/deliveries/01234567-89ab-cdef-0123-456789abcdef/manifest",
  "requiresAccessToken": false,
  "output": [
    { "type": "Patient", "url": "s3://your-bucket/flexpa/.../job.ndjson", "count": 3 },
    { "type": "Observation", "url": "s3://your-bucket/flexpa/.../job.ndjson", "count": 1000 }
  ],
  "error": [],
  "extension": {
    "https://flexpa.com/docs/records/destinations#delivery-manifest": {
      "status": "SUCCEEDED",
      "patientAuthorizationId": "01234567-89ab-cdef-0123-456789abcdef",
      "consentId": "01234567-89ab-cdef-0123-456789abcdef",
      "appExternalUserId": "user-123",
      "syncJobId": "job-id",
      "bytes": 23193904,
      "checksumSha256": "MOFJVevxNSJm3C/4Bn5oEEYH51CrudOzZYK4r5Cfy1g=",
      "checksumType": "COMPOSITE",
      "partSizeBytes": 5242880,
      "partCount": 5
    }
  }
}

#Verify a delivery

Every object that appears in your bucket should correspond to a delivery Flexpa can vouch for. To verify one:

  1. Look it up. Call the list endpoint or the object's manifest.
  2. Check the counts and size. The manifest's bytes and per-type output counts must match the object you downloaded (wc -c, and per-type line counts).
  3. Check the checksum. The manifest's checksumSha256 is the digest S3 verified on receipt — per part for COMPOSITE uploads, over the whole object for FULL_OBJECT — and stored with the object. Because the manifest reaches you over Flexpa API — a separate trust domain from your bucket — a matching digest proves the object's bytes are exactly what Flexpa delivered, even against an actor with write access to the bucket. When checksumSha256 is null (a delivery recorded before checksums existed), skip this step and rely on the count and size checks.

Treat an object with no matching delivery record as an alert: either it was not written by Flexpa, or — in a rare failure mode — Flexpa wrote it but could not record the delivery. Contact support to reconcile it rather than trusting the object.

The fastest checksum check never downloads the object: GetObjectAttributes returns the stored digest and part count, straight from S3. S3 rejects any upload whose provided checksum does not match the received bytes, so the stored digest always reflects the object's actual content — an object re-uploaded by an attacker with different bytes carries a digest that cannot match the manifest without a SHA-256 second preimage.

To verify from the raw bytes instead: for FULL_OBJECT, SHA-256 the file; for COMPOSITE, split the file at partSizeBytes boundaries, SHA-256 each part, concatenate the raw digests, and SHA-256 the result — and confirm the part count matches partCount. This proves the same thing without relying on S3's checksum enforcement, and it covers the bytes you actually downloaded rather than the bytes S3 reports holding.

Checksum verification

# Compare with the manifest's checksumSha256 + partCount
aws s3api get-object-attributes \
  --bucket your-bucket \
  --key "flexpa/.../job.ndjson" \
  --object-attributes Checksum ObjectParts
Status TwitterGitHub

© 2026 Flexpa. All rights reserved.

FHIR® is the registered trademark of Health Level Seven International and its use does not constitute endorsement by HL7.