Skip to main content
Before continuing, make sure you’ve completed the SDK setup and requirements.

Overview

Network issues, timeouts, and client crashes can cause API requests to fail without a clear response. When this happens, you may not know if the email was actually sent. Retrying the request risks sending the same email twice. Idempotency keys solve this problem. By including an Idempotency-Key header in your request, you tell the API to process the request only once. If you retry with the same key, the API returns the original response instead of sending the email again.

How it works

  1. You generate a unique key and include it in the Idempotency-Key header.
  2. The API processes the request and stores the response associated with that key.
  3. If you send the same request again with the same key, the API returns the stored response without reprocessing.
  4. Keys expire after 24 hours. After that, the same key can be reused.

Send an email with an idempotency key

With the SDK

import { randomUUID } from 'node:crypto';

const idempotencyKey = randomUUID();

const { data, error } = await nuntly.emails.send(
  {
    from: 'ray@info.tomlinson.ai',
    subject: 'Order Confirmation',
    to: 'brian67@gmail.com',
    html: '<h1>Your order has been confirmed</h1>',
  },
  {
    headers: {
      'Idempotency-Key': idempotencyKey,
    },
  },
);

if (error) {
  return console.error('Error sending email:', error);
}
console.log(`Email sent ${data.id}`);

With the REST API

curl -X POST https://api.nuntly.com/emails \
  -H 'Authorization: Bearer ntly_your_api_key_here' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
  -d '{
    "from": "ray@info.tomlinson.ai",
    "subject": "Order Confirmation",
    "to": "brian67@gmail.com",
    "html": "<h1>Your order has been confirmed</h1>"
  }'

Bulk emails

For bulk sending, the idempotency key applies to the entire bulk request, not to individual emails within it. If you retry a bulk request with the same key, the full bulk response (including per-email statuses) is returned from cache.
import { randomUUID } from 'node:crypto';

const idempotencyKey = randomUUID();

const { data, error } = await nuntly.emails.bulk.send(
  {
    fallback: {
      from: 'ray@info.tomlinson.ai',
      subject: 'Weekly Newsletter',
    },
    emails: [
      { to: 'carlo43@gmail.com', text: 'Hello Carlo!' },
      { to: 'pink42@yahoo.com', text: 'Hello Pinky!' },
    ],
  },
  {
    headers: {
      'Idempotency-Key': idempotencyKey,
    },
  },
);

Key format

PropertyValue
Minimum length8 characters
Maximum length255 characters
Recommended formatUUID v4
Case sensitiveYes
You can use any string format you prefer, such as UUIDs, ULIDs, or composite keys like order-12345.

Choosing a key strategy

There are three approaches depending on your use case.

Random keys

Use a random UUID when you need retry safety for a single operation. Generate the key before the first attempt and reuse it across retries.
import { randomUUID } from 'node:crypto';

const idempotencyKey = randomUUID();
This is the simplest approach and works well for most cases.

Deterministic keys

Derive the key from a business identifier when you need to prevent duplicates across independent processes. For example, two different servers processing the same order will generate the same key and avoid sending duplicate emails.
const idempotencyKey = `order-receipt-${order.id}`;
This is useful when multiple workers might pick up the same job from a queue, or when a scheduled job might fire twice.

Content-based keys

Hash the email content (or a subset of it) to derive a key when you do not have a stable business identifier but want to prevent sending the same content twice.
import { createHash } from 'node:crypto';

const idempotencyKey = createHash('sha256')
  .update(`${recipient}-${subject}-${body}`)
  .digest('hex');
Choose which fields to include in the hash based on what makes an email “unique” in your context. This approach only works if the hashed fields are stable between attempts. If your email body includes dynamic values that change on each generation (timestamps, formatted dates, random tokens), the hash will differ and the key will not match. In that case, use a deterministic key based on a business identifier instead.

Error handling

The API returns specific error codes for idempotency-related issues.

Invalid key

Returned when the key is empty, too short, or too long.
{
  "error": {
    "status": 400,
    "code": "idempotency_key_invalid",
    "title": "Invalid idempotency key"
  }
}

Key already in progress

Returned when a request with the same key is still being processed. Wait a moment and retry.
{
  "error": {
    "status": 409,
    "code": "idempotency_key_in_progress",
    "title": "Idempotency key is already being processed"
  }
}

Payload mismatch

Returned when you reuse a key with a different request body. The server computes a fingerprint of each request and compares it on retries. If the request body differs from the original, the request is rejected instead of silently ignored. Each key is bound to the exact request body that was sent with it. Use a new key for different requests.
{
  "error": {
    "status": 409,
    "code": "idempotency_key_payload_mismatch",
    "title": "Idempotency key reused with different payload"
  }
}

Behavior on errors

How the API handles idempotency depends on the type of error from the original request:
  • Client errors (4xx): The error response is cached. Retrying with the same key returns the same error. You need to fix the issue and use a new key.
  • Server errors (5xx): The error response is not cached. The key is cleared and you can safely retry with the same key.
  • Successful request with lost response: If the first attempt succeeded but the response was lost (network timeout, client crash), retrying with the same key returns the stored result with the Idempotent-Replayed: true header.
An idempotency key represents a single intent. If a request fails with a 4xx error (such as an unverified domain or an exceeded quota), fix the underlying issue and then use a new key for your next attempt.

Detecting replayed responses

When the API returns a cached response, it includes the Idempotent-Replayed: true header. The response body and status code are identical to the original response. You can use this header to distinguish a replayed response from a fresh one in your logs or monitoring.

Best practices

  • Generate a new unique key for each distinct email you want to send
  • Store the key alongside the operation in your system so you can retry with the same key if needed
  • Use deterministic keys (based on business identifiers) when the same operation can be triggered from multiple code paths

Advanced

SDK retry behavior

The Nuntly SDK automatically retries failed requests up to 2 times with exponential backoff. It retries on connection errors, timeouts, and server errors (5xx). You can configure this behavior per client or per request:
// Global configuration
const nuntly = new Nuntly({ maxRetries: 3 });

// Per-request override
await nuntly.emails.send(emailParams, { maxRetries: 5 });
These automatic retries can cause duplicate email deliveries if a request reaches the server and triggers delivery before the response is lost. Using an idempotency key makes the SDK’s retries safe by default.

Payload fingerprinting

The server computes a fingerprint of each request and compares it when the same idempotency key is reused. If the request differs from the original, the server rejects it with a 409 error (idempotency_key_payload_mismatch) rather than silently ignoring the difference. This prevents a subtle bug: accidentally reusing a key meant for a different email.

Concurrent requests

If two requests with the same key arrive simultaneously, the first to reach the server starts processing. The second receives a 409 error (idempotency_key_in_progress). The processing window is very short, and if the original request stalls, the server automatically clears the lock after a timeout.

Practical patterns

Queue workers

Most message queues guarantee at-least-once delivery, meaning your worker can receive the same message more than once. This typically happens when the acknowledgment is lost after processing. By using the message ID as the idempotency key, duplicate deliveries from the queue do not result in duplicate emails.
async function handleQueueMessage(message: QueueMessage) {
  await nuntly.emails.send(
    {
      from: 'notifications@yourapp.com',
      to: message.body.recipient,
      subject: message.body.subject,
      html: message.body.html,
    },
    {
      headers: {
        'Idempotency-Key': message.id,
      },
    },
  );
}

Scheduled jobs

Cron jobs and scheduled tasks can fire multiple times due to deployment restarts, rolling updates, or clock drift across servers. When two instances of the same job run concurrently, both attempt to send the same email. By combining the job name with the schedule slot, each execution window maps to a single idempotency key.
async function sendDailyReport(date: string) {
  const idempotencyKey = `daily-report-${date}`;

  await nuntly.emails.send(
    {
      from: 'reports@yourapp.com',
      to: 'team@yourapp.com',
      subject: `Daily report for ${date}`,
      html: await renderDailyReport(date),
    },
    {
      headers: { 'Idempotency-Key': idempotencyKey },
    },
  );
}

Webhook-triggered emails

External services like payment providers often retry webhook deliveries when they do not receive a timely response. If your handler sends an email on each webhook call, retried webhooks cause duplicate sends. Using the webhook event ID as the idempotency key ensures the email is sent only once per event, regardless of how many times the webhook is delivered.
app.post('/webhooks/payments', async (req, res) => {
  const event = req.body;

  if (event.type === 'payment.succeeded') {
    await nuntly.emails.send(
      {
        from: 'billing@yourapp.com',
        to: event.data.customerEmail,
        subject: 'Payment received',
        html: renderPaymentConfirmation(event.data),
      },
      {
        headers: {
          'Idempotency-Key': `payment-receipt-${event.id}`,
        },
      },
    );
  }

  res.status(200).send('OK');
});

Send your first email

Get started with the Nuntly API

Bulk email sending

Send multiple emails in a single request

Error handling

Learn about API error responses

API keys

Create and manage your API keys