Developer Guides

Prevent Duplicate Transactional Emails with Idempotency

Stop double receipts and reset emails after timeouts and queue retries. Store a unique send intent before POST /emails. Arawa Mail does not document an Idempotency-Key header today.

Published
Prevent duplicate transactional emails with application-level idempotency

Two identical receipts two minutes apart usually means the checkout already succeeded at the provider and the client never saw the 2xx. The job retried. The customer did not click pay twice. The durable fix is application-level idempotency: a unique send-intent row for (event_type, entity_id), store the provider message id, and never call send again for that intent.

Arawa Mail’s documented Send Email API is Resend-compatible POST /emails with from, to, subject, html/text, cc, bcc, reply_to, headers, and attachments. Reserved headers are ignored. The docs do not list an Idempotency-Key parameter. Do not invent one. Implement the constraint in your app, then use Retrieve Email if you need to look up a stored send by id.

Why one checkout produces two receipts

  • The HTTP client times out after Arawa Mail already returned {"id": "…"}.
  • A queue worker releases the job after a gateway timeout that already accepted the message.
  • The user mashes “resend code” three times in two seconds.

Do not retry a 2xx. Treat an unknown outcome after a timeout as “look up the intent row first.” Laravel ShouldQueue retries are a common source of the second send; the worker tutorial lives separately at the planned queue guide. For inbound events, transactional email webhooks and Arawa Mail outgoing webhooks can themselves arrive more than once — dedupe on id or message_id. Those webhooks are inbound-mailbox notifications, not send-status events.

The send-intent table

Key “order 9912 shipped” separately from “order 9912 receipt.” Same entity, different event types.

ColumnRole
event_typeorder.receipt, order.shipped, auth.password_reset
entity_idOrder id, user id, or other durable id
provider_message_idId returned by POST /emails
statuspendingsent (or failed)
Unique constraint(event_type, entity_id)

Enable Sending must be active. The from domain must be registered, sending-active, and allowed by the API key. Support should read the stored id from the log instead of asking engineering to grep workers — see Support Should See the Transactional Email Log.

Check the intent before POST /emails

Laravel sketch (same idea in Next.js route handlers — see Send transactional email from Next.js):

// unique(event_type, entity_id)
$intent = SendIntent::firstOrCreate(
    ['event_type' => 'order.receipt', 'entity_id' => (string) $order->id],
    ['status' => 'pending']
);

if ($intent->provider_message_id) {
    return $intent->provider_message_id; // already sent
}

$response = Http::withToken($apiKey)
    ->post('https://api.arawamail.com/emails', [
        'from' => '[email protected]',
        'to' => [$order->email],
        'subject' => 'Your receipt',
        'html' => $html,
    ]);

$intent->update([
    'provider_message_id' => $response->json('id'),
    'status' => 'sent',
]);

If two workers race, the unique constraint should reject the second insert. Return the stored id. Do not send twice.

What other APIs do (Resend, for context)

Some providers accept an HTTP Idempotency-Key on POST /emails (Resend documents a 24-hour window and a max key length; SMTP uses a separate header). Recommended key shape in that ecosystem is event-type/entity-id. That is industry context only. Compare products on ArawaMail vs Resend. Do not send Resend’s header to Arawa Mail and expect documented behavior. Do not stuff an idempotency key into custom headers as a substitute — reserved headers are ignored.

How we analyzed this

Claims about Arawa Mail are limited to the public Send Email, Retrieve Email, Enable Sending, API Keys, outgoing webhooks, and Laravel/Next.js quickstart docs as of 21 Sep 2026. No delivery SLA is claimed. Provider-side idempotency is described only where a vendor publishes it.

FAQ

Should you retry a 2xx?

No. Persist the message id from the first accepted response.

Does Arawa Mail document provider-side idempotency today?

No. Implement the unique send-intent row in the application.

What about password-reset button mashing?

Use a short-lived intent keyed to auth.password_reset + user id (or a request token you already mint). Do not send a new message until the previous intent is missing or expired by your own policy.

Simple, transparent plans

Start free. Grow when your email does.

Get one domain, API access and 3,000 transactional emails every month at no cost.

Compare plans