Sign in Create account
Concepts

Idempotency keys: retry sends safely

Retry a send request safely, without the risk of sending the same email twice.

#How to use it

A send request can time out after Postal has already accepted the email. Retrying carelessly sends the email twice. To avoid that, send a unique identifier for the operation in the Idempotency-Key header:

POST /emails

cURL

curl -X POST https://api.postal.so/emails \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Idempotency-Key: order-10482-shipped" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme Store <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your order #10482 is out for delivery",
    "html": "<p>Order <b>#10482</b> is on its way.</p>"
  }'

Node.js

const response = await fetch('https://api.postal.so/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-10482-shipped',
  },
  body: JSON.stringify({
    from: 'Acme Store <[email protected]>',
    to: ['[email protected]'],
    subject: 'Your order #10482 is out for delivery',
    html: '<p>Order <b>#10482</b> is on its way.</p>',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/emails",
    headers={
        "Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
        "Idempotency-Key": "order-10482-shipped",
    },
    json={
        "from": "Acme Store <[email protected]>",
        "to": ["[email protected]"],
        "subject": "Your order #10482 is out for delivery",
        "html": "<p>Order <b>#10482</b> is on its way.</p>",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('emails', [
    'headers' => [
        'Authorization' => 'Bearer '.getenv('POSTAL_API_KEY'),
        'Idempotency-Key' => 'order-10482-shipped',
    ],
    'json' => [
        'from' => 'Acme Store <[email protected]>',
        'to' => ['[email protected]'],
        'subject' => 'Your order #10482 is out for delivery',
        'html' => '<p>Order <b>#10482</b> is on its way.</p>',
    ],
]);

$data = json_decode($response->getBody(), true);

If the same key arrives again within 24 hours, with the same body, Postal does not send again: it returns the stored response of the first request, with the same status and the same ID.

#Rules

  • It applies to POST /emails, POST /emails/batch, POST /events (a repeated event does not start the automation again), POST /contacts/batch and POST /segments/merge. On other endpoints the header is ignored.
  • The key must be 1 to 256 characters long; otherwise the response is 400 invalid_idempotency_key.
  • Keys are scoped to the team: two API keys of the same team share the same idempotency keys.
  • Only successful responses (2xx) are stored. If the first attempt failed with 422 or 429, fix the problem and retry with the same key.
  • Same key with a different body, method or path: 409 invalid_idempotent_request.
  • Same key while the first request is still in progress: 409 concurrent_idempotent_requests. Wait a moment and retry.

#Choosing the key

Use something that identifies the reason for the send, not the attempt: order-10482-shipped, password-reset-8f2a91 or a UUID generated and saved together with the order. Generating a new UUID on every attempt defeats the protection.