<!-- https://cutedyno.com/docs/guides/idempotency -->

# Idempotency

Make retries safe so a network timeout can never publish the same post twice.

A request that times out tells you nothing about whether it was applied. The post may have been created and the response lost, or the request may never have arrived. Retrying is the only reasonable response, and without idempotency, retrying is how you publish the same thing twice.

Send an `Idempotency-Key` header on writes:

```bash
curl -X POST https://api.cutedyno.com/v1/posts \
  -H "Authorization: Bearer $CUTEDYNO_API_KEY" \
  -H "Idempotency-Key: 8f14e45f-ea0c-4d7a-9f1a-2b3c4d5e6f70" \
  -H "Content-Type: application/json" \
  -d '{"content":"Shipping today.","accountIds":["…"],"publishNow":true}'
```

If that exact request already succeeded, CuteDyno returns the stored response instead of doing the work again, with an `Idempotency-Replayed: true` header so you can tell.

## What the key covers

Keys are scoped to the API key that used them and stored for **24 hours**. Within that window:

| Situation | What happens |
| --- | --- |
| First request with this key | Executed normally, and the response is stored. |
| Same key, same body | The stored response is replayed. Nothing runs twice. |
| Same key, different body | `409` with code `idempotency_key_reused`. |
| Same key, but the first attempt failed | Executed again. Failures are not stored. |
| Key older than 24 hours | Treated as new. |

That third row is a guardrail, not an inconvenience. If the same key arrives with a different body, one of the two requests is not what you meant, and replaying an unrelated response would be worse than an error.

The fourth row is what makes retry loops work: a `500` or a `429` leaves no stored response, so retrying with the same key genuinely retries.

## Which endpoints accept it

Every `POST`, `PUT`, `PATCH`, and `DELETE`. It matters most on the ones that cost something:

- [`POST /v1/posts`](/docs/api/create-post) — a duplicate is a duplicate post on a real account.
- [`POST /v1/posts/:id/publish`](/docs/api/publish-post) and [`/retry`](/docs/api/retry-post) — same.
- [`POST /v1/profiles`](/docs/api/create-profile) — a duplicate splits one customer across two tenants.
- [`POST /v1/api-keys`](/docs/api/create-api-key) — a duplicate leaves an unaccounted secret in existence.
- [`POST /v1/webhooks`](/docs/api/create-webhook) — a duplicate doubles every delivery.

`GET` requests ignore the header. They are already idempotent.

## Choosing a key

Any string works, but the choice decides what the key protects you from.

**A random UUID per attempt** protects against nothing. Each retry looks like a new request.

**A random UUID per logical operation**, generated once and reused across retries, protects against retries within that operation. This is what the Node SDK does automatically: one key per call, reused across its internal retries.

```typescript
// Generated once, reused across the SDK's own retries.
await cutedyno.posts.create({ content: 'Shipping today.', accountIds });
```

**A key derived from your own data** protects against everything, including a crash between the request and recording its result, and a job that runs twice:

```typescript
await cutedyno.posts.create(
  { content: post.body, accountIds: post.accountIds, scheduledAt: post.scheduledAt },
  { idempotencyKey: `post-${post.id}` }
);
```

Now the operation is safe no matter how many times your worker runs it. If your scheduler fires twice, or a deploy restarts a process mid-flight, the second attempt replays rather than publishes.

This is the pattern to use for anything driven by your own database rows. The one thing to watch: the body must be identical, or you get `idempotency_key_reused`. If the row can change between attempts, include a version in the key — `post-${post.id}-v${post.revision}`.

## A retry loop worth copying

```typescript
import { CuteDynoError } from '@cutedyno/node';

async function createPostWithRetry(input: PostInput, key: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    try {
      return await cutedyno.posts.create(input, { idempotencyKey: key });
    } catch (error) {
      const retryable = error instanceof CuteDynoError && error.isRetryable;
      if (!retryable || attempt === 3) throw error;
      await sleep(error.retryAfter ? error.retryAfter * 1000 : 2 ** attempt * 500);
    }
  }
}
```

Three things make it correct: the key is created outside the loop, only retryable errors are retried, and `Retry-After` is honoured when present. The SDK already does all three; this is what to write if you are not using it.

## Common mistakes

**Regenerating the key inside the retry.** The most common one. Each attempt then looks new, and idempotency does nothing.

**Reusing one key for a batch.** Ten posts under one key means one post and nine replays of its response. One key per post.

**Assuming a replay means failure.** `Idempotency-Replayed: true` on a `201` means the post exists. Treat it as success.

**Deriving keys from a timestamp.** `post-${Date.now()}` is a random key with extra steps.
