Reference

Conditional Requests (ETags)

Use ETags and the If-Match header for optimistic concurrency control — prevent silent data loss when multiple clients update the same resource.

Last updated 2026-06-11

The Core API attaches an ETag header to every single-resource response. Clients can send this value back in an If-Match header on update or delete requests to ensure they are operating on the latest version of the resource. If the resource has changed since the ETag was issued, the API returns 412 Precondition Failed instead of silently overwriting the other client’s changes.

Quick start

Fetch a resource and note the ETag response header:

curl -i https://api.authpi.com/v1/accounts/{account_id}/issuers/{issuer_id}/users/{user_id} \
  -H "Authorization: Bearer $TOKEN"
HTTP/2 200
ETag: "1709139600000"
Content-Type: application/json

{"data": {"id": "usr_abc", "name": "Alice", "updated_at": 1709139600000, ...}}

When updating the resource, include the ETag in an If-Match header:

curl -X PATCH https://api.authpi.com/v1/accounts/{account_id}/issuers/{issuer_id}/users/{user_id} \
  -H "Authorization: Bearer $TOKEN" \
  -H "If-Match: \"1709139600000\"" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith"}'

If no one else modified the user since you last read it, the update succeeds and you receive a new ETag:

HTTP/2 200
ETag: "1709139700000"

{"data": {"id": "usr_abc", "name": "Alice Smith", "updated_at": 1709139700000, ...}}

If someone else changed the user in the meantime, you get a 412:

HTTP/2 412

{"error": "precondition_failed", "error_description": "User has been modified since the provided ETag. Fetch the latest version and retry.", "currentETag": "\"1709139700000\"", "retryable": false}

How it works

ETag value

The ETag is a strong ETag (no W/ prefix) whose value is the resource’s updated_at timestamp in milliseconds, quoted per RFC 7232. If the resource has never been updated, created_at is used instead.

ETag: "<updated_at ?? created_at>"

Since this timestamp is already present in the response body, the ETag reveals no additional information.

When ETags are returned

The API sets the ETag header on all 200 and 201 responses that contain a single resource ({ data: { ... } }). Specifically:

Response typeETag?Example
Single resource (GET, PATCH, POST create)YesGET /users/{id}
List responseNoGET /users
Delete (204 No Content)NoDELETE /users/{id}
Error response (4xx, 5xx)NoAny error

If-Match validation

When an If-Match header is present on a PATCH or DELETE request, the API compares the provided ETag against the resource’s current timestamp inside the Durable Object that owns the resource. Because Durable Objects are single-threaded per resource ID, the comparison and the subsequent mutation happen atomically — there is no time-of-check-to-time-of-use (TOCTOU) gap.

Supported If-Match values:

ValueBehavior
"<timestamp>"Matches if the resource’s current ETag equals this value
"<ts1>", "<ts2>", ...Matches if any value in the comma-separated list matches
*Always matches (unconditional)
W/"<timestamp>"Rejected — weak ETags are not accepted (strong comparison only)

Backward compatibility

The If-Match header is entirely optional. When omitted, the request proceeds unconditionally (last-write-wins). This means existing clients and integrations continue to work without any changes.

Error response

When the ETag does not match, the API returns 412 Precondition Failed:

{
  "error": "precondition_failed",
  "error_description": "User has been modified since the provided ETag. Fetch the latest version and retry.",
  "currentETag": "\"1709139700000\"",
  "retryable": false
}

The currentETag field contains the resource’s actual current ETag, so you can inspect how stale your version is. However, you should still re-fetch the resource to get the full current state before re-applying your changes.

Which endpoints support If-Match?

All PATCH and DELETE endpoints that operate on a single resource support the If-Match header. This includes:

ResourcePATCHDELETE
AccountsPATCH /accounts/{id}
IssuersPATCH .../issuers/{id}DELETE .../issuers/{id}
UsersPATCH .../users/{id}DELETE .../users/{id}
ClientsPATCH .../clients/{id}DELETE .../clients/{id}
OrganizationsPATCH .../organizations/{id}DELETE .../organizations/{id}
Organization GroupsPATCH .../groups/{id}DELETE .../groups/{id}
AgentsPATCH .../agents/{id}DELETE .../agents/{id}
Auth MethodsPATCH .../auth-methods/{id}DELETE .../auth-methods/{id}
WebhooksPATCH .../webhooks/{id}DELETE .../webhooks/{id}
API KeysPATCH .../api-keys/{id}DELETE .../api-keys/{id}
Personal TokensDELETE .../personal-tokens/{id}
NotesPATCH .../notes/{id}DELETE .../notes/{id}

Best practices

Always store the ETag from the response you based your changes on

When you fetch a resource, save the ETag header alongside the data. When you send an update, include it as If-Match. This ensures your update is based on the version you actually read.

// Fetch the resource
const res = await fetch(`${BASE}/users/${userId}`, { headers: { Authorization: `Bearer ${token}` } });
const etag = res.headers.get("ETag");
const user = (await res.json()).data;

// Later — update with optimistic concurrency
const updateRes = await fetch(`${BASE}/users/${userId}`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${token}`,
    "If-Match": etag,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "New Name" }),
});

Handle 412 with a read-modify-write retry

When you receive a 412, the correct recovery is to re-read the resource, re-apply your changes (merging if necessary), and retry:

async function safeUpdate(url, updates, token, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    // 1. Read current state
    const getRes = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const etag = getRes.headers.get("ETag");
    const current = (await getRes.json()).data;

    // 2. Merge your changes
    const merged = { ...updates };

    // 3. Write with If-Match
    const patchRes = await fetch(url, {
      method: "PATCH",
      headers: {
        Authorization: `Bearer ${token}`,
        "If-Match": etag,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(merged),
    });

    if (patchRes.status === 412) {
      // Resource was modified — retry with fresh state
      continue;
    }

    return patchRes;
  }

  throw new Error("Max retries exceeded — resource is under heavy contention");
}

Use ETags in AI agent workflows

When an AI agent performs a multi-step workflow (e.g., read user → decide → update user), other agents or human users may modify the same resource between steps. Without If-Match, the agent would silently overwrite their changes. With If-Match, the agent receives a clear 412 signal and can re-read, re-evaluate, and retry.

// Agent workflow: conditionally update user metadata
const res = await fetch(userUrl, { headers: { Authorization: `Bearer ${token}` } });
const etag = res.headers.get("ETag");
const user = (await res.json()).data;

// Agent decides what to change (may take time)
const updates = await agentDecide(user);

// Safe update — won't clobber changes made while agent was thinking
const patchRes = await fetch(userUrl, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${token}`,
    "If-Match": etag,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(updates),
});

if (patchRes.status === 412) {
  // Re-read and let agent re-evaluate with fresh data
}

Combine with idempotency keys

ETags and idempotency keys solve different problems and work well together:

  • Idempotency keys protect against duplicate creation (POST retries)
  • ETags protect against stale updates (PATCH/DELETE races)

For maximum safety, use both:

# Create with idempotency (protects against retries)
curl -X POST .../users \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{"name": "Alice"}'

# Update with If-Match (protects against races)
curl -X PATCH .../users/usr_abc \
  -H "If-Match: \"1709139600000\"" \
  -d '{"name": "Alice Smith"}'

When not to use If-Match

Omitting If-Match is perfectly valid when:

  • You are the sole writer — only one system modifies the resource
  • Last-write-wins is acceptable — e.g., toggling a boolean setting where races are harmless
  • You are performing a delete and don’t care whether the resource was modified — the result (deletion) is the same either way

The If-Match header is a tool for correctness, not a requirement. Use it when concurrent modifications would cause problems.