> Markdown version of https://authpi.com/docs/reference/conditional-requests/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# 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.

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:

```bash
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:

```bash
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](https://httpwg.org/specs/rfc7232.html). 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 type | ETag? | Example |
|---------------|-------|---------|
| Single resource (GET, PATCH, POST create) | Yes | `GET /users/{id}` |
| List response | No | `GET /users` |
| Delete (204 No Content) | No | `DELETE /users/{id}` |
| Error response (4xx, 5xx) | No | Any 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 &mdash; there is no time-of-check-to-time-of-use (TOCTOU) gap.

**Supported If-Match values:**

| Value | Behavior |
|-------|----------|
| `"<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 &mdash; 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`:

```json
{
  "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:

| Resource | PATCH | DELETE |
|----------|-------|--------|
| Accounts | `PATCH /accounts/{id}` | &mdash; |
| Issuers | `PATCH .../issuers/{id}` | `DELETE .../issuers/{id}` |
| Users | `PATCH .../users/{id}` | `DELETE .../users/{id}` |
| Clients | `PATCH .../clients/{id}` | `DELETE .../clients/{id}` |
| Organizations | `PATCH .../organizations/{id}` | `DELETE .../organizations/{id}` |
| Organization Groups | `PATCH .../groups/{id}` | `DELETE .../groups/{id}` |
| Agents | `PATCH .../agents/{id}` | `DELETE .../agents/{id}` |
| Auth Methods | `PATCH .../auth-methods/{id}` | `DELETE .../auth-methods/{id}` |
| Webhooks | `PATCH .../webhooks/{id}` | `DELETE .../webhooks/{id}` |
| API Keys | `PATCH .../api-keys/{id}` | `DELETE .../api-keys/{id}` |
| Personal Tokens | &mdash; | `DELETE .../personal-tokens/{id}` |
| Notes | `PATCH .../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.

```typescript
// 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:

```typescript
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 &rarr; decide &rarr; 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.

```typescript
// 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:

```bash
# 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** &mdash; only one system modifies the resource
- **Last-write-wins is acceptable** &mdash; e.g., toggling a boolean setting where races are harmless
- **You are performing a delete** and don't care whether the resource was modified &mdash; 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.