Docs
guides · sending

Idempotency & retries

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 POSTs, 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.

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

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\u2019re in.</p>"
  }'
The SDK maps it for youPass 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.

02 How it behaves

  1. 1

    First request with a key

    The send is processed normally and the result — the message id and status — is stored under the key.
  2. 2

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

    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.
409 conflict
{
  "error": {
    "type": "conflict",
    "message": "Idempotency-Key was reused with a different request body"
  }
}

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

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

A stable key
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 });
Anchor the key to a business eventTie 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.
Keys on replies
// Replies accept an idempotency key too.
await drin.emails.reply(
  inboundMessageId,
  { html: "<p>Thanks — we're on it.</p>" },
  { idempotencyKey: `reply:${inboundMessageId}` },
);

05 How the SDK retries

The TypeScript SDK retries transient failures (HTTP 429, 5xx, and network errors) with exponential backoff up to maxRetries2 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.
No key means no auto-retry on POSTIf 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.
Batches are keyed as a wholeOne key covers an entire sendBatch request. A replay returns the original results array rather than re-sending every item. See Batch & scheduled.

06 Next