Writing back to a practice management system without duplicates
Your worker posts a patient payment to the practice management system. The request times out. The worker retries. The practice now shows two payments, and the patient's balance is wrong by one copay.
What HTTP promises
RFC 9110 section 9.2.2 defines an idempotent method as one where the intended effect of multiple identical requests equals the effect of one. PUT, DELETE and the safe methods are idempotent. POST is not. The RFC says a client should not automatically retry a non-idempotent request unless it knows the semantics are idempotent, or can detect that the original was never applied. A PUT interrupted before any response can be retried. A POST cannot, unless the server gives you a way to make it safe.
The IETF draft Idempotency-Key header, at revision 08 as of October 2026, standardizes that way. The client sends a unique key with a POST or PATCH. On a retry after the original completed, the server returns a response consistent with the completed request. On a retry while the original is in flight, it returns 409. On a key reused with a different payload, it returns 422. If the vendor API does not document support for this header, you implement the equivalent on your side.
What FHIR gives you
The FHIR R4 RESTful API has the tools built in:
- Conditional create. POST with an If-None-Exist header holding search parameters. No match creates the resource and returns 201. One match returns 200 and creates nothing. Several matches return 412.
- Version-aware update. The server returns an ETag with the version id. Send it back in If-Match on PUT or PATCH. A stale version returns 412 instead of overwriting.
- Transaction Bundle. All entries succeed or all fail. Each entry can carry its own ifNoneExist, and entries can reference each other with urn:uuid fullUrls that the server resolves (Bundle).
Whether a vendor honors these headers is a vendor fact. eClinicalWorks lists update on six resources, Patient, Encounter, Coverage, ServiceRequest, Immunization and MedicationAdministration, each behind a contract (documentation). The public documentation does not state whether If-None-Exist or If-Match are honored. The open source Python and TypeScript connectors send each create as a transaction Bundle, which their README describes as how eClinicalWorks accepts writes. They require a resource id on update. Test the conditional behavior against the vendor sandbox before you depend on it.
Idempotency keys on your side
Give each intended write a key before you send it. Derive the key from the business fact, not from the attempt. A payment key is the patient id, the payment source, the amount, the payment date and the check or card reference. An appointment key is the patient id, the provider id and the start time. A note key is the encounter id, the note type and a hash of the content. A demographics key is the patient id, the set of fields and the version of your source record.
Store the key with a state: pending, sent, confirmed or failed. Insert the row before you send. If the insert conflicts, an earlier attempt exists, and you read its state instead of sending again. After the response, record the vendor's returned id and version. A retry of a write in state sent becomes a read before it becomes a second POST.
Where the vendor accepts a client reference, put the key there. In FHIR that is an identifier with your own system URI. The key is then a search parameter, and conditional create works against it:
POST /fhir/r4/Patient
If-None-Exist: identifier=https://yourapp.example/ids|8f2c0a1e
{ "resourceType": "Patient",
"identifier": [{ "system": "https://yourapp.example/ids", "value": "8f2c0a1e" }],
... }
Duplicate detection without a key field
Where a payment or appointment endpoint has no client reference field, detect duplicates by reading before and after. Before the write, search the target window: payments for the patient on that date with that amount, or appointments for that provider in that slot. If the record exists, link it to your key and mark the key confirmed. After the write, search again and expect exactly one more record. Two more means a duplicate, and your key row records which one you created so you can void it.
Searches lag. Some systems index asynchronously, and the record you created is not searchable for a short time. Retry the post-write read with backoff, stop after a bounded number of attempts, and mark the write unconfirmed rather than failed.
When the system has no update endpoint
Vendors expose create on more resources than update. eClinicalWorks documents six update endpoints against a longer list of create endpoints (documentation). When the record you need to change has no update, in order of preference:
- Correct by compensation. Post a reversing payment and a new payment. Cancel the appointment and book a new one. The audit trail stays honest, and this is how an 835 reversal already works.
- Append instead of change. For notes, add an addendum and leave the original.
- Use a record type the system already versions. Coverage works this way: end the old coverage by its period and add a new one. This is how the US Core Coverage profile models change through period and status.
- Queue a task for a person in the vendor UI, with the diff and your key, and record the task id as the confirmation.
Do not update by delete and create. Linked records, such as charges on an appointment or payments on a claim, lose their parent.
Confirming the write landed
A 2xx response does not prove the write landed. Confirm with a read. FHIR returns a Location header of the form [base]/[type]/[id]/_history/[vid] on 201; read it back by id and compare the fields you sent with the fields you got. Store the vendor id, the version id or ETag and the read timestamp against your key. If the system has no read for that record type, confirm through the side effect: the patient balance moved by the amount, or the slot is no longer free.
Rate limits apply to confirmation reads. The eClinicalWorks limit is 500 calls per minute per base URL. Once an application exceeds it, every call from that application is blocked for the rest of the minute (documentation). Batch confirmations and spread them over time.
Keep the raw request and response for every write, with the key. When the practice asks why a payment appears twice, you can show both attempts, both responses, and which one you voided.