> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Safe retries for write requests via the Idempotency-Key header.

export const Def = ({term, type, defaultValue, required, children}) => <>
    <dt className="gv-def-list__term">
      <span className="gv-def-list__name">{term}</span>
      {type && <span className="gv-def-list__type">{type}</span>}
      {required && <span className="gv-def-list__required">required</span>}
      {defaultValue !== undefined && <span className="gv-def-list__default">
          default <code>{defaultValue}</code>
        </span>}
    </dt>
    <dd className="gv-def-list__description">{children}</dd>
  </>;

export const DefList = ({children}) => <dl className="gv-def-list">{children}</dl>;

export const NumberedStep = ({n, title, children}) => <li className="gv-step">
    <span className="gv-step__index">{n}</span>
    <div className="gv-step__body">
      <h3 className="gv-step__title">{title}</h3>
      <div className="gv-step__content">{children}</div>
    </div>
  </li>;

export const NumberedSteps = ({children}) => <ol className="gv-steps">{children}</ol>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="IDEMPOTENCY" />

Attach an `Idempotency-Key` header to any write request and the server will return the cached response on a retry instead of executing the operation a second time. This is how you guard against duplicate side effects from network failure, browser double-submit, or dispatcher re-delivery. By the end of this page you will know what the key is, when to send it, and the rules the cache enforces.

## How it works

<NumberedSteps>
  <NumberedStep n={1} title="Generate a unique key">
    Before each logical write, generate a key — `crypto.randomUUID()` or any RFC 4122 UUID works. Hold the key in scope so retries reuse the same value.
  </NumberedStep>

  <NumberedStep n={2} title="Send the key on the write request">
    Attach the header on any POST, PUT, PATCH, or DELETE:

    ```bash theme={null}
    curl -X POST https://api.gavai.app/v1/workspaces/acme/tokens \
      -H "Authorization: Bearer gak_live_xxxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
      -d '{"scopes": ["pages:read", "pages:write"]}'
    ```
  </NumberedStep>

  <NumberedStep n={3} title="Server caches the response">
    The response is stored against the tuple of `(token_id, idempotency_key, route, body_sha256)`. Cache lifetime is **24 hours**.
  </NumberedStep>

  <NumberedStep n={4} title="Retries return the cached response">
    A repeat request with the same key inside 24 hours returns the original response without executing the operation again. The reply carries `Idempotent-Replayed: true` so you can tell.
  </NumberedStep>
</NumberedSteps>

## Where it applies

| Method class                     | Behavior                                                                    |
| -------------------------------- | --------------------------------------------------------------------------- |
| `GET`                            | Naturally idempotent. Header is ignored if sent.                            |
| `POST`, `PUT`, `PATCH`, `DELETE` | Header is honored. Cached responses are replayed inside the 24-hour window. |

Writes that create new resources — minting API tokens, sending invitations, attaching domains — should always carry a key. Without one, a retry creates a second resource.

## Key requirements

<DefList>
  <Def term="Uniqueness">Per logical operation. Reusing a key for a different intended operation will silently replay the first operation's response.</Def>
  <Def term="Length">Up to 255 characters.</Def>
  <Def term="Case">Case-sensitive — `ABC` and `abc` are different keys.</Def>
  <Def term="Recommended format">UUID v4 (`crypto.randomUUID()` in browsers and Node 19+).</Def>
  <Def term="Body match">The same key with a different request body returns `422` with `error.code = "IDEMPOTENCY_MISMATCH"`. The server will not execute a modified request under a seen key.</Def>
</DefList>

## Cache window

The cached response lives **24 hours** from the first successful request. After that, the entry expires and the same key is treated as a new attempt. The window covers dispatcher retries, CI re-runs, and same-day human double-submits.

The cache is eventually consistent across regions — propagation is typically under 60 seconds. A retry hitting a different region inside that window may execute again. For payment-class operations, treat idempotency keys as necessary but not sufficient and combine with application-level deduplication.

## TypeScript SDK

`@gavai/sdk-typescript` generates and attaches an `Idempotency-Key` for every POST, PATCH, PUT, and DELETE call. Pass `{ idempotencyKey }` explicitly when you need to control the key across a retry loop:

```ts theme={null}
import { GavaiClient } from "@gavai/sdk-typescript";

const client = new GavaiClient(process.env.GAVAI_API_KEY!);

const key = crypto.randomUUID();

// Both calls carry the same key — only one token is minted.
const token = await client.workspaces.tokens.create("acme",
  { scopes: ["pages:read"] }, { idempotencyKey: key });

const same  = await client.workspaces.tokens.create("acme",
  { scopes: ["pages:read"] }, { idempotencyKey: key });

// same.id === token.id — the second call returned the cached response.
```

## Next

<Card title="Errors and retries" icon="triangle-alert" href="/api-reference/errors">
  Status codes, retry classes, and the `Retry-After` contract on 429s.
</Card>
