Skip to content
ONPADDocs

Docs / API

Idempotency

Why every send needs an Idempotency-Key, and how to choose one.

View as Markdown

Every POST /messages request must carry an Idempotency-Key header. It is the difference between a retry that is safe and a retry that messages your customer twice.

The problem it solves

Your server posts a message. The network drops before the response arrives. Your code does not know whether the message was queued or not.

Without idempotency you have two bad choices: retry and risk sending twice, or do not retry and risk not sending at all.

With an idempotency key, retrying is simply safe. The second request returns the first one's result and sends nothing.

How it works

Idempotency-Key: order-10482-shipped

Rules: 8 to 120 characters, made of letters, digits, ., _, : and -.

When a request arrives, ONPAD looks for an earlier message in your workspace with the same key:

  • No earlier message → the message is queued. 202 Accepted.
  • Earlier message, identical body → the original is returned with "duplicate": true. 200 OK. Nothing new is sent.
  • Earlier message, different body → 409, with "This Idempotency-Key was already used for a different request." Nothing is sent.

The comparison is on the whole request body, normalised so that key order does not matter. {"to":"91...","type":"text"} and {"type":"text","to":"91..."} count as the same request.

Keys are scoped to your workspace. Another workspace using the same string does not collide with yours.

Choosing a key

Use something from your own data that identifies this exact send. The best keys are derived, not random, because a derived key is the same on a retry:

order-10482-shipped
invoice-2026-0931-reminder
booking-77341-confirmation-2
appointment-5521-reminder-24h

Do not use a fresh random string per attempt. A new UUID on every retry defeats the whole mechanism — each attempt looks like a new message and each one sends.

Do not reuse a key for a different message. Sending order-10482-shipped and later reusing it for a delivery notification gets a 409, and the second message never goes.

If one order genuinely needs several messages, put the purpose in the key: order-10482-shipped, order-10482-delivered.

Retrying correctly

key = f"order-{order_id}-shipped"

for attempt in range(3):
    try:
        response = post_message(key, payload)
        break
    except NetworkError:
        time.sleep(2 ** attempt)

The key is computed once, outside the loop. Every attempt carries the same one, so at most one message is ever sent, no matter how many attempts run.

What it does not do

It does not retry delivery for you. It makes your retries safe. Delivery retries happen separately inside ONPAD — 3 attempts with backoff.

It does not expire quickly. Keys are kept with the message. Reusing a key from months ago still returns that old message rather than sending.

It does not protect against your own duplicate logic. Two different code paths that build different keys for the same event will both send. Derive the key from the event, not from where you are in the code.

If you get a 409

You reused a key with a different body. Two possibilities:

  1. The key is too generic — something like order-10482 used for several different messages about that order. Make it specific.
  2. The body changed between attempts — a timestamp or a random value inside the payload, so the retry no longer matches. Build the payload once and reuse it, exactly like the key.

Still stuck?

Ask ONPAD, the assistant inside your workspace, can answer questions about your own data — how a specific campaign performed, why one message failed, what your limits are right now.

Ask ONPAD in your workspaceEmail support@onpad.in