Skip to main content
Request idempotency allows you to safely retry API requests without worrying about duplicate operations. When you include an Idempotency-Key header with your request, our API will ensure that multiple requests with the same key produce the same result.
Idempotency is particularly important for critical operations like sending emails, creating resources, or processing payments where duplicate actions could cause problems.

How It Works

When you make a request with an Idempotency-Key header:
  1. First Request: The API processes your request normally and stores the response
  2. Subsequent Requests: If you retry with the same key, the API returns the stored response instead of processing the request again
  3. Server Errors Are Retriable: If the original attempt failed with a server error (HTTP 5xx) or never completed, no response is stored — retrying with the same key safely re-executes the request
  4. Automatic Cleanup: Stored idempotency responses expire after 24 hours. The exception is API key creation: because its response carries a one-time secret_key, the replayable response expires after 5 minutes
Idempotency is supported on all POST endpoints that create or modify resources:
  • API Keys: Create API keys
  • Domains: Create domains
  • Messages: Send messages
  • Account Members: Add team members
  • Suppressions: Create email suppressions
  • Routes: Create message routes
  • Webhooks: Create webhook endpoints
  • SMTP Credentials: Create SMTP credentials
  • Sub Accounts: Create sub accounts
  • Sub-Account API Keys: Create sub-account API keys
API-key creation endpoints — Create API Key and Create Sub-Account API Key — return a one-time secret_key in the response.Their idempotent replay response is encrypted and expires after 5 minutes (instead of the usual 24 hours). Within that window, an exact retry with the same Idempotency-Key replays the same secret_key. After it, the secret can no longer be retrieved — if you lose it, create a new key.Every other create endpoint returns non-secret data and is replayable for the full 24 hours.
  • Header Name: Idempotency-Key
  • Key Format: Any string up to 255 characters
  • Uniqueness: Keys are scoped to your account
  • Expiration: Stored responses expire after 24 hours — except API key creation, whose secret-bearing response expires after 5 minutes
  • Request Method: Only POST requests support idempotency
Requests are matched based on:
  • Account ID
  • Idempotency key
  • Request method and path, including path parameters
  • Request body content (SHA256 hash)
If you reuse the same key with a different request body — or send the same body to a different endpoint or resource — the API returns 422 Unprocessable Entity.

Key Selection Best Practices

A client generates an idempotency key, which is a unique key that the server uses to recognize subsequent retries of the same request. How you create unique keys is up to you, but we suggest using V4 UUIDs, or another random string with enough entropy to avoid collisions. Idempotency keys are up to 255 characters long.

Key Generation Examples

// Using crypto.randomUUID() (Node.js 14.17+)
const idempotencyKey = crypto.randomUUID();
// Result: "550e8400-e29b-41d4-a716-446655440000"

// Using a library like uuid
import { v4 as uuidv4 } from 'uuid';
const idempotencyKey = uuidv4();

// Custom format with timestamp
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2);
const idempotencyKey = `msg_${timestamp}_${random}`;
// Result: "msg_1705317045123_k2j5h8n3m1"
Key Collision Risk: With V4 UUIDs, the probability of generating duplicate keys is approximately 1 in 5.3 x 10^36. For practical purposes, this is negligible even at massive scale.

Usage Example

Include the Idempotency-Key header with any POST request:
curl -X POST https://api.ahasend.com/v2/accounts/acct_123/messages \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: msg_20240115_001" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": "user@example.com",
    "subject": "Welcome!",
    "html": "<h1>Welcome to our service!</h1>"
  }'

Response Behavior

The API responds differently based on the idempotency key status:
Status: 200 OK (or appropriate success status)Headers:
  • Standard response headers
  • No special idempotency headers
Body: Normal response content
{
  "id": "msg_abc123",
  "status": "queued",
  "created_at": "2024-01-15T10:30:00Z"
}

Best Practices

Unique Keys

Use unique, descriptive keys that won’t conflict with other operations. Consider including timestamps or UUIDs.
Good: user_123_welcome_20240115_001
Bad: request_1

Retry Logic

Implement exponential backoff when retrying requests. Always use the same idempotency key for retries.
const maxRetries = 3;
let attempt = 0;

while (attempt < maxRetries) {
  try {
    return await makeRequest(idempotencyKey);
  } catch (error) {
    if (error.status === 409) {
      // Request in progress, wait and retry
      await sleep(Math.pow(2, attempt) * 1000);
    } else {
      throw error;
    }
  }
  attempt++;
}

Key Expiration

Stored responses expire after 24 hours — don’t reuse a key after this period, as the behavior is undefined. API key creation is shorter: its one-time secret_key is only replayable for 5 minutes.

Error Handling

Handle different response codes appropriately:
  • 409: Concurrent request in progress — wait for the Retry-After interval, then retry with the same key
  • 422: Key reused with a different request — use a new idempotency key, or retry with the original payload and endpoint
  • 5xx: Transient failure — retry with the same idempotency key
  • 2xx/4xx with Idempotent-Replayed: true header: replayed original outcome

Error Scenarios

This happens when you make concurrent requests with the same idempotency key.What it means: Another request with the same key is currently being processed.What to do: Wait for the interval indicated by the Retry-After response header, then retry with the same key. The retry will either get a 409 again (still processing), the stored result once complete, or — if the original request never completed (e.g. it was interrupted) — it will re-execute the request once the in-flight lease expires (at most 5 minutes).
Response
{
  "message": "A request with this idempotency key is already in progress"
}
If the original request failed with an HTTP 5xx error, no response is stored for the idempotency key.What it means: The failure is treated as transient, and the key remains usable.What to do: Retry with the same idempotency key — the request is re-executed as if it were the first attempt. (Client errors — HTTP 4xx — behave differently: they are deterministic outcomes and are replayed on retry, just like successes.)
If you use the same idempotency key with different request data — or with the same data against a different endpoint or resource — the API returns 422 Unprocessable Entity.What it means: The request doesn’t match the original request for that key.What to do: Ensure you’re using the exact same request (payload and endpoint) when retrying, or use a different idempotency key for different requests.
Response
{
  "message": "idempotency key was already used with a different request payload"
}

Implementation Details

The following technical details are provided for transparency but are not required for basic usage.

Request Hashing

The API uses SHA256 hashing of the request method, path, and body to detect changes between requests with the same idempotency key. This ensures that different requests don’t accidentally match — including the same payload sent to a different resource.

Concurrency Protection

Claiming an idempotency key happens atomically; While a request is in flight, concurrent requests with the same key receive 409 Conflict with a Retry-After header instead of waiting.

Execution Leases

An in-flight request holds a lease on its idempotency key for up to 5 minutes. If the original request never completes (for example, it was interrupted by a deploy or crash), the key unblocks automatically when the lease expires and the next retry re-executes the request.

Storage Duration

Idempotency records are automatically cleaned up after 24 hours. This prevents the idempotency table from growing indefinitely while providing a reasonable retry window. Responses that contain a one-time secret — API key creation and sub-account API key creation — are stored encrypted and their replayable response expires after 5 minutes. Within that window an exact retry replays the same secret_key; afterward the secret can no longer be returned.
For high-volume applications, consider implementing client-side deduplication in addition to server-side idempotency to reduce unnecessary API calls.