Claim Responses (277 / 835) | Medplum

This guide explains how Medplum ingests Stedi inbound claim responses — 277CA claim acknowledgments and 835 Electronic Remittance Advice (ERA) — and how to set up the two delivery paths: webhooks (the primary, near-real-time path) and an optional poller (a catch-up safety net).

This workflow is handled by the Stedi claim-response bots. Please contact the Medplum team to get access to these bots.

Overview

After you submit a professional claim, payers respond asynchronously:

Response X12 Meaning
Claim acknowledgment 277CA The clearinghouse/payer accepted or rejected the claim for processing
Remittance / payment 835 ERA Adjudication and payment details for the claim

Stedi notifies you when a response is ready. The notification delivers only a transactionId — Medplum then fetches the full report from Stedi's 277 report or 835 report APIs (and, for 835, the ERA PDF).

Storage model

Reports are stored with minimal translation. Each report is saved verbatim as a DocumentReference:

transaction.processed.v2 (transactionId)

fetch 277/835 report

(

best-effort link

Delivery path

Webhook bot

(primary)

Poller bot

(catch-up)

Stedi

DocumentReference

(raw report JSON)

existing ClaimResponse

Bots and operations

Bot identifier Operation Role
stedi-claim-response-webhook $stedi-claim-response-webhook Receives Stedi transaction.processed.v2 deliveries. In production Stedi calls it directly via its identifier-based $execute URL; the operation is provided for manual replay/testing
stedi-claim-response-poller $stedi-poll-responses Catch-up: polls Stedi for inbound 277/835 transactions missed by the webhook
install-stedi $stedi-install One-time setup: creates the webhook ClientApplication, prints the webhook URL, and optionally sets project secrets

What gets stored

Transaction DocumentReference(s) created
277 One DocumentReference (application/json) containing the raw 277 report
835 One DocumentReference (application/json) containing the raw 835 report, plus (best-effort) one DocumentReference (application/pdf) for the ERA PDF

Each stored DocumentReference carries these identifiers:

System Value
https://www.stedi.com/transactions/inbound The inbound Stedi transaction id (the ERA PDF uses <transactionId>/pdf)
https://www.stedi.com/response-type 277 or 835
https://www.stedi.com/events The originating webhook event id (present only when delivered via webhook)

ClaimResponse linking

The submit-claim bot stamps the patient control number (X12 CLM01) onto the Claim as an identifier with system https://www.stedi.com/patient-control-number. The payer echoes that same value back on the 277/835, so the claim-response flow can correlate an inbound report to the originating Claim and its ClaimResponse.

When a match is found, a flat extension is added to the ClaimResponse referencing the stored report DocumentReference:

Transaction Extension URL on ClaimResponse
277 https://www.stedi.com/fhir/StructureDefinition/claim-response-277-report
835 https://www.stedi.com/fhir/StructureDefinition/claim-response-835-report

Linking is best-effort: if the control number, Claim, or ClaimResponse cannot be found, the report is still stored in full as a DocumentReference — no data is lost. A single claim per transaction is assumed (only the first control number found in a report is correlated), which matches how claims are submitted (one Claim per Stedi transaction).

Idempotency

Processing is idempotent on the Stedi inbound transactionId (not the webhook event.id). Before fetching a report, the flow checks whether a DocumentReference already exists with identifier https://www.stedi.com/transactions/inbound|<transactionId>; if so, the transaction is skipped.

This makes it safe for the webhook and the poller to overlap — re-processing the same transaction is a no-op — and safely ignores webhook replays delivered with a new event.id.

Setting up webhooks

Webhooks are the primary, near-real-time delivery path. Setup follows four steps: run the install bot, grab the client credentials it creates, create the webhook in Stedi, and add the transaction-processed event binding.

Step 1 — Run the Stedi Install bot in your own project

As a project admin, invoke the $stedi-install operation at the base of your project. This runs the Stedi Install bot, which creates a ClientApplication that Stedi uses to authenticate its webhook calls and prints the exact webhook URL to configure. You can optionally set the Stedi project secrets at the same time (any omitted secret is left untouched, so this is safe to re-run):

POST {base}/fhir/R4/$stedi-install

Content-Type: application/fhir+json

{
 
  "resourceType": "Parameters",

"parameter": [
  
    { "name": "STEDI_CLAIM_API_KEY", "valueString": "<YOUR_STEDI_CLAIM_API_KEY>" },
  
    { "name": "STEDI_INSURANCE_API_KEY", "valueString": "<YOUR_STEDI_INSURANCE_API_KEY>" },
  
    { "name": "STEDI_CLAIM_TEST_MODE", "valueBoolean": false }
  
  ]
}

The operation is idempotent — re-running it does not create duplicate clients. It returns an OperationOutcome whose text summarizes:

Polling (catch-up)

The poller is an optional safety net that backfills any responses the webhook missed. It polls Stedi's Poll Transactions API for INBOUND 277/835 transactions since the last checkpoint and stores each report the same way the webhook does. It is idempotent on the inbound transaction id, so it never double-stores a report the webhook already ingested.

Running the poll operation

Invoke $stedi-poll-responses at the type level. It takes no input:

POST {base}/fhir/R4/ClaimResponse/$stedi-poll-responses
const result = await medplum.post(

medplum.fhirUrl('ClaimResponse', '$stedi-poll-responses')
);

The operation returns a summary of the run:

{

"ok": true,

"since": "2026-06-24T00:00:00.000Z",

"checkpoint": "2026-07-01T17:05:00.000Z",

"processedCount": 3,

"skippedCount": 12
}
Field Meaning
since Start of the poll window (the previous checkpoint)
checkpoint New checkpoint timestamp, captured before fetching so transactions arriving mid-run are not skipped next time
processedCount Inbound 277/835 transactions newly stored on this run
skippedCount Transactions skipped (already processed, or not inbound 277/835)

Querying stored reports

Stored reports are DocumentReference resources, searchable by the identifiers above:

GET {base}/fhir/R4/DocumentReference?identifier=https://www.stedi.com/response-type|277

GET {base}/fhir/R4/DocumentReference?identifier=https://www.stedi.com/response-type|835

GET {base}/fhir/R4/DocumentReference?identifier=https://www.stedi.com/transactions/inbound|<transactionId>

When a report was linked to a ClaimResponse, you can also reach it from the ClaimResponse extension (…/claim-response-277-report or …/claim-response-835-report).

Limitations