Concurrency
When two processes update the same object, one can silently overwrite the other: your CRM sync and a colleague in the console both edit a contact, and the slower write wins. Campaigns and contacts use ETags to prevent that. This page explains the headers, the errors you can get and the read-modify-write loop that handles them.
ETags
Responses that return a campaign or a contact include an ETag header identifying that version:
Text
Returns an ETag | Operations |
|---|---|
| Contacts | GET/contacts/{contact_id}, POST/contacts, POST/contacts/upsert, PATCH/contacts/{contact_id} |
| Campaigns | GET/campaigns/{campaign_id}, POST/campaigns, PATCH/campaigns/{campaign_id}, and the start, pause, resume and stop actions |
The value is opaque: store and send it exactly as received, including the double quotes. It changes whenever the object changes, including changes made in the console and, for a campaign, changes of state while it runs. Only the ETag of a single campaign or contact is meant for If-Match: read the single object before you update it, and do not use an ETag header from any other response (for example a list). A replayed answer (Idempotent-Replayed: true) carries the ETag stored with it, which may be out of date.
If-Match on PATCH
PATCH/contacts/{contact_id} and PATCH/campaigns/{campaign_id} require If-Match with the ETag from your latest read. First read the contact:
Text
Then send the change with that ETag in If-Match (shown as <etag>; send the value exactly as received, quotes included):
Text
| Situation | Response |
|---|---|
If-Match matches the current version | 200 OK with the updated object and its new ETag. |
If-Match missing | precondition_required (428). Nothing changed. |
| The object changed since you read it | version_conflict (412). Nothing changed. |
Both errors have retry: "no": resending the same request fails the same way. PATCH does not take an Idempotency-Key. (PATCH of a webhook endpoint takes neither header.)
If-Match: *
If-Match: * matches any current version: the update is applied without the concurrency check, so it can overwrite a change you have not seen. It still satisfies the header requirement (no 428). Use it only where last-write-wins is what you want, for example a one-off correction by an operator; in automated syncs, send the real ETag.
Other conditional headers
Only campaigns and contacts carry an ETag, and only If-Match on their PATCH is checked. No other response has an automatic ETag, and conditional GET headers (If-None-Match, If-Modified-Since) are ignored: a response is never 304 Not Modified.
Campaigns have one more rule: they can be edited in every status except running (pause first; editing a running campaign answers campaign_state_conflict), and edits take effect at the next start.
Resolving a conflict
If the contact changed between your read and your PATCH (a colleague edited it in the console, for example), the update is refused and nothing changes:
JSON
Do not resend your old body with a new ETag: that would overwrite the change you have not seen, which is exactly what the check is there to prevent. Instead:
- Read the object again (the same
GETas above). Note its newETag. - Work out your change again from what you just read. It may now be unnecessary.
- Send the
PATCHwith the newIf-Match. - Repeat a small number of times (three is plenty); if it keeps conflicting, something else is writing to the object constantly, so stop and report it.
The same loop also handles a lost response: if a PATCH times out, start again from the read. If the first attempt had applied, the read shows your change is already there and nothing more is sent.
The complete runnable example creates a test contact, then reads, changes and updates it with If-Match, reading again after a 412.
Complete runnable example
Update a contact without overwriting a change you have not seen:
The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.
- cURL script (bash)
etag-update.sh - JavaScript (Node.js 20+)
etag-update.mjs - Python 3.9+ (requests)
etag-update.py
Concurrency elsewhere
- Creates and actions (
POST,DELETE) are made safe to retry by idempotency keys, not ETags. - Campaign actions are serialised: only one start or resume runs per campaign at a time, and a second one answers
campaign_operation_in_progresswithRetry-After. - Contacts by phone number: two creates for the same number answer
contact_already_existsfor the second one. Use POST/contacts/upsertto create or update in one request. - Events can arrive out of order. Use the object's own status and timestamps, not the arrival order, to decide which is newer.