Skip to main content

Claim Responses (277 / 835)

This guide explains how Medplum ingests Stedi inbound claim responses: 277CA claim acknowledgments and 835 Electronic Remittance Advice (ERA). Webhooks are the primary path; an optional poller can catch missed events.

Overview

After you submit a professional claim, payers respond asynchronously:

ResponseX12FHIR written
Claim acknowledgment277CAClaimResponse
Remittance / payment835 ERAClaimResponse + PaymentReconciliation

Stedi notifies you via transaction.processed.v2 webhooks. The webhook delivers only the transactionId — your bot must fetch the full JSON from the 277 report or 835 report APIs. For 835, Medplum also fetches the ERA PDF and stores it as a DocumentReference linked from ClaimResponse.

Bots

Bot identifierRole
stedi-claim-response-webhookReceives Stedi transaction.processed.v2 events via Bot/$execute
stedi-claim-response-pollerOptional catch-up: polls Stedi for inbound 277/835 missed by the webhook

Both bots use project secret STEDI_CLAIM_API_KEY (same key as claim submission).

Correlation identifiers on Claim

The submit bot writes identifiers used to match inbound responses:

SystemPurpose
https://www.stedi.com/claimsStedi correlation ID
https://www.stedi.com/correlation-idSame correlation ID (277 batch matching)
https://www.stedi.com/patient-control-numberPCN sent on the 837 (835 matching)
https://www.stedi.com/transactions/outboundOutbound 837 transaction ID when returned

Ensure patient-control-number is set before submit — the submit bot assigns a PCN from the Claim id if you do not override it.

Idempotency

Processing is idempotent on Stedi inbound transactionId (not webhook event.id):

  • 277: skip if ClaimResponse exists with identifier system https://www.stedi.com/transactions/inbound
  • 835: skip if PaymentReconciliation exists with the same inbound identifier

Replays of the same webhook with a new event.id are safely ignored.

Configure Stedi webhooks

  1. Deploy stedi-claim-response-webhook and note the Bot/{id}.
  2. Create a ClientApplication with an AccessPolicy that can execute the bot.
  3. In Stedi Webhooks:
    • Credential set: API Keys — Authorization: Bearer {access_token} from client credentials
    • Endpoint: POST https://api.medplum.com/fhir/R4/Bot/{botId}/$execute
    • Event binding: Transaction processed (optionally filter to 277 and 835)
  4. Grant the bot an AccessPolicy to write Claim, ClaimResponse, PaymentReconciliation, Organization, DocumentReference, and Binary.

See Consuming Webhooks for AccessPolicy examples.

Webhook payload

{
"detail-type": "transaction.processed.v2",
"id": "8a9fc08a-24b2-4eeb-af7c-f96376ea471e",
"detail": {
"transactionId": "7647d644-9348-4596-a3b4-6830b8b48cc8",
"x12": { "transactionSetIdentifier": "277" }
}
}

The bot must return HTTP 200 within 5 seconds. Acknowledge quickly; processing runs in the same invocation for v0.

Optional poller (catch-up)

The webhook is the primary path. stedi-claim-response-poller is an optional catch-up bot that polls Stedi's Poll Transactions API for INBOUND 277 and 835, storing its checkpoint on a Basic resource (identifier: https://www.stedi.com/poller|stedi-claim-response-poller).

Scheduling the poller is left to the customer. Medplum cron is bound to the Bot resource and runs in the bot's home project, so scheduling cron on a shared poller bot does not poll per customer project. A customer that wants automated catch-up should deploy and schedule the poller in its own project, where its cron executes in that project's context.

Query claim status in FHIR

GET ClaimResponse?request=Claim/{claimId}&_sort=-_lastUpdated
GET ClaimResponse?request=Claim/{claimId}&identifier=https://www.stedi.com/response-type|277
GET ClaimResponse?request=Claim/{claimId}&identifier=https://www.stedi.com/response-type|835

Test workflow

Use Stedi's test claims workflow with payer STEDITEST and STEDI_CLAIM_TEST_MODE=true on submit. Stedi generates test 277CA and 835 responses you can ingest via webhook or poller.

Limitations (v0)

  • Professional (837P) only
  • No file.failed.v2 alerting
  • No Real-Time Claim Status (276/277) API
  • Provider UI status display is not included (query ClaimResponse from your app)