Notideus
Tutorials

Idempotency

Safe retries with idempotency keys.

The problem

Networks are unreliable. Your client can lose its connection after the API stored your email but before the response arrived — and your natural reaction is to retry. Without a guard, that retry creates a duplicate send (and double quota usage). Delivery is at-least-once, so your sending code must be safe to run twice.

How it works

Add an idempotency_key field to the request body. The key is unique per team. The first request with a given key stores its row and returns 201 Created; any later request with the same key returns the original stored row unchanged — same id, original field values, no merge — with 200 OK instead.

First send (201 Created):

{
  "id": "01a0a11c-188e-7c9e-9b69-753fcb4c8d0d",
  "team_id": "00000000-0000-0000-0000-000000000001",
  "domain_id": "01a0a118-2c7b-7da6-aa31-ceb20daf80c6",
  "template_id": null,
  "from_address": "Notideus <noreply@demo.dev>",
  "to_emails": ["jane@example.com"],
  "subject": "Welcome to Notideus",
  "html": "<p>Hi Jane,</p><p>Your account is ready.</p>",
  "size_kb": 0.04,
  "tags": ["welcome"],
  "status": "queued",
  "idempotency_key": "docs-quickstart-1",
  "created_at": "2026-09-14T18:09:28.206638Z",
  "requeued_at": null,
  "ses_message_id": null,
  "broadcast_id": null,
  "contact_id": null,
  "unsubscribe_token": null
}

Replay of the same idempotency_key (200 OK):

{
  "id": "01a0a11c-188e-7c9e-9b69-753fcb4c8d0d",
  "team_id": "00000000-0000-0000-0000-000000000001",
  "domain_id": "01a0a118-2c7b-7da6-aa31-ceb20daf80c6",
  "template_id": null,
  "from_address": "Notideus <noreply@demo.dev>",
  "to_emails": ["jane@example.com"],
  "subject": "Welcome to Notideus",
  "html": "<p>Hi Jane,</p><p>Your account is ready.</p>",
  "size_kb": 0.04,
  "tags": ["welcome"],
  "status": "queued",
  "idempotency_key": "docs-quickstart-1",
  "created_at": "2026-09-14T18:09:28.206638Z",
  "requeued_at": null,
  "ses_message_id": null,
  "broadcast_id": null,
  "contact_id": null,
  "unsubscribe_token": null
}

Decode:

  • Same id — the replay echoes the stored row; it is not a new email, and the second request body is not merged into the first.
  • Same original values — status, created_at, addresses: the first stored row wins, byte for byte.

With the SDK

Pass idempotency_key exactly as above — key choice and replay semantics are unchanged. The SDK adds one layer on top: when a send carries a key, it automatically retries network failures, 5xx and 429 (honoring Retry-After), because the key makes the retry safe. Keyless sends are never auto-retried.

A replay resolves normally with the original stored row — the SDK does not distinguish 200 from 201, and email.id is stable across attempts.

In PHP the behavior is identical: keyed sends are automatically retried on network failures, 5xx and 429 (honoring Retry-After), keyless sends are never auto-retried, and a replay resolves normally with $email['id'] stable across attempts.

Scope

  • POST /v1/emails — one key per email.
  • POST /v1/emails/batch — each item carries its own key; replays are resolved per item.
  • POST /v1/whatsapp/messages — keyed per key and recipient: a logical message to three phones replays per phone, never for just some of them.

Key choice

Derive the key deterministically from your own data — order-12345-welcome, invoice-6789-receipt — never a random value per attempt. The common pattern: generate one UUID per logical operation, store it in your database next to the order or user, and reuse it on every retry of that operation.

What idempotency does not cover

Idempotency keys dedupe sends only — they do not apply to contact writes. That is fine, because contact syncs are naturally idempotent: bulk upsert is keyed on email address, so re-running the same sync is safe by construction. See Syncing contacts.
Copyright © 2026