FHIR conditional update id mismatch: 400 by the spec, 200 on some servers

A conditional update finds a resource by search, but the body names a different id. FHIR R4 says refuse with 400. In our October 2026 tests, HAPI 8.12.0 applied it. Here is why it matters.

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.

A FHIR server can accept a write and quietly drop a field. Deedglass reads the resource back and names what differs. Made-up resource.

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 id from 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:

  1. Resolve before you write. Run the search first. If it returns zero or more than one, stop.
  2. 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
  3. Prefer a plain update with If-Match once you know the id: PUT /Patient/p-12 with the version you read. Then a concurrent change gets refused too.1
  4. Read back the resource you meant to change, and confirm the field changed on that resource and not on another.
  5. Record the search, the match and the id you wrote, so a reviewer can see the chain later.
  6. 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.

The takeaway

R4 says refuse a conditional update whose body id differs from the match; HAPI 8.12.0 applied it with 200 while two other setups refused, so check ids yourself.

Read nextFHIR conditional create: when If-None-Exist stores nothing, and when it stores two Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1HL7 FHIR R4: RESTful API, conditional update · hl7.org · checked 27 October 2026
  2. 2Deedglass research: FHIR write integrity 2026 · deedglass.com · checked 27 October 2026
  3. 3hapifhir/hapi-fhir issue #8389 · github.com · checked 27 October 2026
  4. 4Deedglass for hospitals: the 15-minute pilot · deedglass.com · checked 27 October 2026

Published 27 October 2026, sources last checked 27 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing