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

# Idempotency & retries

> Pass an Idempotency-Key header to make a POST safe to retry. Drin remembers the key for 24h per sending project; the SDK only auto-retries POSTs that carry one.

Networks fail mid-flight. An idempotency key lets you retry a send without fear of delivering it twice — Drin recognizes the repeat and returns the original result instead of sending again.

Send operations are `POST`s, which aren't safe to blind-retry: a request can succeed on the server even though the response never reaches you. An **idempotency key** closes that gap. Attach one and Drin records the outcome under it; a retry with the same key replays that outcome rather than performing a second send.

## The Idempotency-Key header

Send a unique `Idempotency-Key` header on any send. It works on `POST /v1/emails`, `POST /v1/emails/batch`, and `POST /v1/emails/{id}/reply`. The value is yours to choose — a UUID or, better, a business identifier tied to the operation.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.drin.run/v1/emails \
    -H "Authorization: Bearer $DRIN_API_KEY" \
    -H "Idempotency-Key: welcome-user-4815" \
    -H "Content-Type: application/json" \
    -d '{
      "from": { "email": "hello@acme.com" },
      "to": [{ "email": "customer@example.com" }],
      "subject": "Welcome aboard",
      "html": "<p>You’re in.</p>"
    }'
  ```

  ```typescript Node.js theme={null}
  import { DrinClient } from "@drin00/sdk";

  const drin = new DrinClient({ apiKey: process.env.DRIN_API_KEY });

  await drin.emails.send(
    {
      from: { email: "hello@acme.com" },
      to: [{ email: "customer@example.com" }],
      subject: "Welcome aboard",
      html: "<p>You’re in.</p>",
    },
    { idempotencyKey: "welcome-user-4815" },
  );
  ```
</CodeGroup>

<Note>
  **The SDK maps it for you.** Pass `idempotencyKey` in the per-call options (the second argument to `send`, `sendBatch`, and `reply`) and the client sets the `Idempotency-Key` header on the request.
</Note>

## How it behaves

<Steps>
  <Step title="First request with a key">
    The send is processed normally and the result — the message `id` and status — is stored under the key.
  </Step>

  <Step title="Retry with the same key and body">
    Drin returns the **original** result without sending again. Your caller sees the same `id` as if the first response had arrived.
  </Step>

  <Step title="Reuse the key with a different body">
    Drin refuses the request with a `409 conflict` — a reused key must describe the same operation. Use a fresh key for a different message.
  </Step>
</Steps>

```json 409 conflict theme={null}
{
  "error": {
    "type": "conflict",
    "message": "Idempotency-Key was reused with a different request body"
  }
}
```

## The 24-hour window, per project

A key is remembered for **24 hours**, scoped to the **sending project** it was used under. Two consequences:

* **Retry inside 24h** and the original result is replayed. After the window expires, the same key is treated as new and the message can send again — so keep retries well within a day.
* **Keys don't collide across projects.** The same string used by two different projects is two independent keys. With an account-wide key, the project is the one you name via `X-Drin-Product` (or the SDK's `sender`) — see [Authentication](/authentication).

## Choosing a key

The key must be *stable across retries of the same operation* and *unique across different operations*. Derive it from the work you're doing, not from the moment you do it — generating a new random value on each attempt defeats the purpose.

```typescript A stable key theme={null}
import { randomUUID } from "node:crypto";

// Derive ONE key per logical operation and reuse it across retries.
// A natural business id is ideal — here, "welcome the user who just signed up".
const key = `welcome:${userId}`;
// Or a random UUID persisted with the job so a retry sends the SAME key:
// const key = job.idempotencyKey ?? (job.idempotencyKey = randomUUID());

await drin.emails.send(payload, { idempotencyKey: key });
```

<Tip>
  **Anchor the key to a business event.** Tie the key to the thing that triggered the email — `welcome:<userId>`, `receipt:<orderId>`, `reset:<tokenId>`. Two requests for the same event share a key and dedupe; two genuinely different emails never collide.
</Tip>

```typescript Keys on replies theme={null}
// Replies accept an idempotency key too.
await drin.emails.reply(
  inboundMessageId,
  { html: "<p>Thanks — we're on it.</p>" },
  { idempotencyKey: `reply:${inboundMessageId}` },
);
```

## How the SDK retries

The TypeScript SDK retries transient failures (HTTP `429`, `5xx`, and network errors) with exponential backoff up to `maxRetries` — **2** by default. Its retry rule is deliberately conservative:

* **GET** and other reads are always safe to retry, so they are.
* **POST is retried only when it carries an idempotency key.** A `POST` without one is sent exactly once — on a transient failure the SDK surfaces the error rather than risk a duplicate send.

<Warning>
  **No key means no auto-retry on POST.** If you want the SDK to automatically recover a flaky send, you **must** pass `idempotencyKey`. Otherwise a single network blip turns into a failed call you have to handle yourself — and retrying it by hand without a key risks double-sending.
</Warning>

<Note>
  **Batches are keyed as a whole.** One key covers an entire `sendBatch` request. A replay returns the original `results` array rather than re-sending every item. See [Batch & scheduled](/sending/batch).
</Note>

## Next

<CardGroup cols={2}>
  <Card title="Send email" icon="bolt" href="/sending">
    Where the idempotency key attaches.
  </Card>

  <Card title="Batch & scheduled" icon="bolt" href="/sending/batch">
    How keys apply to a 100-message batch.
  </Card>

  <Card title="Errors" icon="shield" href="/errors">
    The `conflict` type and the rest of the error model.
  </Card>

  <Card title="TypeScript SDK" icon="code" href="/sdk/typescript">
    Retry, timeout, and `maxRetries` options.
  </Card>
</CardGroup>
