The short answer
A FHIR conditional update (PUT [base]/Patient?identifier=...) finds the resource by search instead of by id. If the search finds exactly one resource but the body carries a different id, FHIR R4 says the server returns 400 Bad Request.1 In our October 2026 study, HAPI FHIR JPA Server 8.12.0 applied such an update with 200 in both configurations we ran, while the HAPI public server’s 8.13.10-SNAPSHOT build and the Firely Server 6.11.0 sandbox refused it with 400. HAPI tracks this as issue #8389.2 Until you know what your server does, check the id match in your own code before you send.
Deedglass
The server said “saved”. Deedglass looked.
Deedglass reads it back
The five cases in R4
R4 lists what a server does with a conditional update, depending on how many resources match and whether the body carries an id:1
| Matches | Body id | Server does |
|---|---|---|
| None | None | Creates the resource |
| None | Given | Treats it as update-as-create |
| One | None, or the same as the match | Updates the match |
| One | Different from the match | 400 Bad Request |
| More than one | Any | 412 Precondition Failed |
The fourth row is the safety rule. The search says “this patient”; the body says “that patient”. The two disagree, so the server shouldn’t guess which one the client meant.
What three servers did
On 26 October 2026 we sent synthetic writes to four setups and read every one back: HAPI FHIR JPA Server 8.12.0 run locally twice (starter defaults; request validation on), the HAPI public test server (build 8.13.10-SNAPSHOT of 25 September 2026) and the Firely Server 6.11.0 public sandbox.2
| Setup | Body id differs from the match | Reply |
|---|---|---|
| HAPI 8.12.0, defaults | Applied | 200 |
| HAPI 8.12.0, request validation on | Applied | 200 |
| HAPI public server, 8.13.10-SNAPSHOT | Refused | 400 |
| Firely Server 6.11.0 sandbox | Refused | 400 |
Source:2. The behaviour is version-dependent: the newer HAPI snapshot build refused, as R4 says. If you run HAPI, check which version you have and read the issue for status.3 We don’t call this a defect in anyone’s product; we name the versions so you can test your own.
A worked example
An agent is told to update the phone number for the patient with MRN 00042. It searches, gets two candidate records back from an earlier step, and builds its update from the wrong one.
Code A request one program sends to another over the web.
PUT /fhir/Patient?identifier=https://hospital.example/mrn|00042
Content-Type: application/fhir+json
{ "resourceType": "Patient", "id": "p-77",
"identifier": [{ "system": "https://hospital.example/mrn", "value": "00042" }],
"telecom": [{ "system": "phone", "value": "+91 90000 00000" }] }
The search matches Patient p-12. The body says p-77. R4 says 400.1 A server that applies it anyway has to pick one meaning, and whichever it picks, the client’s intent and the stored result can now differ without any error in the reply.
The failure is easy to miss in a log. The agent’s own record says “updated Patient MRN 00042, 200 OK”. That line is true and still misleading.
How an agent gets here
Mismatched ids aren’t exotic. They come from ordinary agent behaviour:
- Stale context. The agent read one patient earlier in the session and reuses that resource as the template for a later update.
- Copy-and-edit. The model copies a resource from a search result list and edits the identifier but not the id.
- Merged records. Two records for the same person exist, and the search and the body each point at one.
- Tool glue. A wrapper fills
idfrom one call’s output and the search parameters from another’s.
Each produces a request where the search and the body disagree. On a server that refuses, you get a clear 400. On one that applies, you get a 200 and a record that may not be the one anyone meant.
Guarding the write yourself
You don’t need to wait for a server upgrade. Add these checks in the code or gateway that sends agent writes:
- Resolve before you write. Run the search first. If it returns zero or more than one, stop.
- Compare ids. If the body has an id and it differs from the match, refuse locally, the same way R4 says the server should.1
- Prefer a plain update with
If-Matchonce you know the id:PUT /Patient/p-12with the version you read. Then a concurrent change gets refused too.1 - Read back the resource you meant to change, and confirm the field changed on that resource and not on another.
- Record the search, the match and the id you wrote, so a reviewer can see the chain later.
- Test your server with a deliberate mismatch in a sandbox. It takes one request and tells you which row of the table you’re in.
| Check | Catches |
|---|---|
| Search returns exactly one | Ambiguous and missing records |
| Body id equals match id | The mismatch in this post |
If-Match on the version read |
Concurrent edits |
| Read-back on the intended id | A write that landed elsewhere, or not at all |
Questions people ask
What should a FHIR server return when the conditional update body id doesn’t match?
400 Bad Request, per R4.1 Two of the setups we tested did; HAPI 8.12.0 applied the update instead.2
Is it safer to drop the id from the body?
With no id in the body and one match, R4 says the server updates the match.1 That avoids this mismatch but not a wrong match; still check that the search returns exactly the resource you meant.
How do I find out what my server does?
Send a deliberate mismatch to a sandbox with synthetic data and read the result back. Our study page has the exact cases we ran.
To see a read-back catch a write that said success without setting up a server, run npx deedglass health pilot, a 15-minute pilot on synthetic data on your own machine.4 The hospital overview and the record spec show where checks like these fit. To try approvals and read-backs on a real database in your browser, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.