FurniOpsDocs

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.

HeaderOnEffect
If-None-MatchGET304 Not Modified, with no body, when nothing changed since you read it.
If-MatchPATCH of a source or a webhook endpointThe 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 304

A 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)"}'
AnswerMeaningWhat to do
200Applied. The answer is the new object.For another change later, read it again for its new ETag.
412 precondition_failedSomeone changed it since you read it.Read it again, re-apply your change to the new state, and retry.
428 precondition_requiredYou 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:

rename.ts
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.

On this page