API

Idempotencyv1

Prevent duplicate sends during retries with deterministic idempotency keys.

Why it matters

Retries are inevitable in distributed systems: client timeouts, transient 502s, deploys, provider hiccups, and flaky networks. Without idempotency, a retried create-style request can produce duplicate side-effects.

For an email API, that usually means duplicate sends, which is one of the fastest ways to lose user trust.

Idempotency lets you safely retry by telling SendLib: "this is the same logical operation as before".

How it works

For create-style endpoints, include a stable Idempotency-Key header. SendLib uses it to deduplicate requests over a retention window.

bash
curl -X POST https://api.sendlib.com/v1/transmissions \
  -H "Authorization: Bearer $SENDLIB_API_KEY" \
  -H "Idempotency-Key: order-1042:receipt:v1" \
  -H "Content-Type: application/json" \
  -d '{"recipients":[{"email":"user@example.com"}],"content":{"subject":"Receipt","text":"Thanks!"}}'

Don't generate a new key on retry

If you generate a new key each attempt (e.g. a new UUID per retry), you lose the protection. The key must be stable across every attempt of the same logical operation.

What happens on retry?

When you retry with the same Idempotency-Key:

  • If the request is identical, SendLib returns the stored response (no duplicate side-effect).
  • If the request body differs, SendLib responds with a conflict (typically 409) because a key can only represent one logical operation.

Choosing good keys

Good keys are deterministic and map 1:1 to a business operation.

Key patternExampleBest for
Business object IDorder-1042:receiptOne send per order / invoice
Composite keyuser-789:welcome:2026-02-06Periodic sends with a time bucket
Persisted UUID550e8400-e29b-41d4-a716-446655440000When you can't derive a natural key

Persist the key before calling the API

Generate the idempotency key, store it in your DB, then call SendLib. On retry, load the same key from your DB instead of regenerating it.

Idempotency is necessary but not sufficient. A good client retry policy is:

  1. Set timeouts for every request.
  2. Retry only on transient failures (429, 5xx, network errors).
  3. Use exponential backoff with jitter.
  4. Cap retries and surface an error when you exceed the limit.

Implementation examples

ts
// Node.js / TypeScript
export async function sendWithRetry(payload: unknown, idempotencyKey: string) {
  const maxRetries = 3;
 
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch("https://api.sendlib.com/v1/transmissions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SENDLIB_API_KEY}`,
        "Idempotency-Key": idempotencyKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });
 
    if (res.ok) return await res.json();
 
    // Non-retryable client errors
    if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 409) {
      throw new Error(`SendLib client error ${res.status}: ${await res.text()}`);
    }
 
    // Conflict means the key was reused with different inputs.
    if (res.status === 409) {
      throw new Error("Idempotency conflict (409). Reused key with different request body.");
    }
 
    const delayMs = Math.min(1000 * 2 ** attempt, 10_000) + Math.floor(Math.random() * 500);
    await new Promise((r) => setTimeout(r, delayMs));
  }
 
  throw new Error("SendLib request failed after retries");
}

Next