ETags and concurrent changes
Cache reads with If-None-Match, and stop two writers overwriting each other with If-Match.
Every successful GET that answers JSON carries a strong ETag: a hash of the exact bytes sent.
It changes whenever the representation does, including when you ask for another
API version. Treat it as opaque and send it back in one of two headers.
| Header | On | Effect |
|---|---|---|
If-None-Match | GET | 304 Not Modified, with no body, when nothing changed since you read it. |
If-Match | PATCH of a source or a webhook endpoint | The change is applied only if the object is still as you read it. |
Cache reads with If-None-Match
curl -i https://api.furniops.com/v1/products/prod_034XqBaL2mRLTZPXyRbaUc \
-H "Authorization: Bearer $FURNIOPS_API_KEY"
# HTTP/2 200
# etag: "0P9bvlb3Ke8YqoFZtq1cxQa7Rk2"
curl -i https://api.furniops.com/v1/products/prod_034XqBaL2mRLTZPXyRbaUc \
-H "Authorization: Bearer $FURNIOPS_API_KEY" \
-H 'If-None-Match: "0P9bvlb3Ke8YqoFZtq1cxQa7Rk2"'
# HTTP/2 304A 304 still counts against your read rate budget, but sends no body. Use
it when you poll an object you already hold.
Prevent lost updates with If-Match
Two people (or a person and a job) read a source, both change it, and the second write silently
undoes the first. If-Match stops that: send the ETag you read, and the change is refused if
the source changed in between.
# 1. Read, and keep the ETag.
ETAG=$(curl -si https://api.furniops.com/v1/sources/src_034XqBaL2mRLTZPXyRbaUc \
-H "Authorization: Bearer $FURNIOPS_API_KEY" | awk 'tolower($1)=="etag:" {print $2}' | tr -d '\r')
# 2. Change it, only if it is still as read.
curl -X PATCH https://api.furniops.com/v1/sources/src_034XqBaL2mRLTZPXyRbaUc \
-H "Authorization: Bearer $FURNIOPS_API_KEY" \
-H "If-Match: $ETAG" \
-H "Content-Type: application/json" \
-d '{"name": "Northwind (EU)"}'| Answer | Meaning | What to do |
|---|---|---|
200 | Applied. The answer is the new object. | For another change later, read it again for its new ETag. |
412 precondition_failed | Someone changed it since you read it. | Read it again, re-apply your change to the new state, and retry. |
428 precondition_required | You sent no If-Match. | Read the object first. |
If-Match is required on PATCH /v1/sources/{id} and PATCH /v1/webhook-endpoints/{id}.
With the SDK
Every result carries its response as lastResponse, so the ETag is one call away. Header
parameters become call options: sources.update and webhookEndpoints.update require
ifMatch (the types will not compile without it). A
read-modify-write loop that retries on a conflict:
const id = "src_034XqBaL2mRLTZPXyRbaUc";
for (let attempt = 0; attempt < 3; attempt++) {
const source = await furniops.sources.get(id);
const etag = source.lastResponse.headers.get("etag") ?? "";
try {
// Sent as If-Match: a change made by someone else since the read answers 412.
await furniops.sources.update(id, { name: `${source.name} (EU)` }, { ifMatch: etag });
break;
} catch (error) {
if (error instanceof ConflictError && error.status === 412) continue; // read again
throw error;
}
}Retrying a 412 blindly with the same ETag never succeeds: always read again first, and decide
whether your change still makes sense against what someone else wrote.