BeaconBox
Esc

Type to search.

Dashboard

Idempotency

Required on every write. What a key scopes to, what happens when you reuse one, and why a key per intent beats a key per request.

Six operations require an Idempotency-Key header. A request to one without it is rejected with 422 and common.validation_failed, naming the header.

Requires one Why
POST /messages Sends a real person an email and can spend a credit.
POST /messages/batch The same, up to a hundred times.
POST /messages/{id}/retract Cancels queued sends and reports how many.
POST /messages/{id}/sms Spends a credit.
POST /messages/{id}/whatsapp Spends a credit.
POST /recipients/{email}/whatsapp/erase Deletes data and issues a receipt.

Nothing else does, and that is the rule rather than an oversight: the header is required exactly where repeating a call would repeat an effect somebody can see. Setting a recipient’s number twice leaves one number, and revoking a key twice leaves it revoked, so those are safe to repeat on their own terms.

Sending the header anyway on an operation that does not require it is harmless. Both SDKs do, on every POST.

shell
curl https://api.beaconbox.eu/api/v1/messages \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711-shipped" \
-d '{"recipient_email": "buyer@example.com",
     "subject": "Your order has shipped",
     "body": "Tracking XY123456789EE."}'

What happens on a repeat

Same key, same body. The stored response is replayed, with the status code it originally returned. Nothing is written and nothing is sent a second time.

Same key, different body. 409 with idempotency.key_reused. Replaying would answer a different update with the old result and never deliver the new one, so this is a hard error rather than a quiet replay.

Same key, still in flight. 409 with idempotency.request_in_progress. Retry; it will replay the stored response once the first call finishes.

Keys are scoped to your business and to the operation. The same key on a push and on a retract are two different things and do not collide.

A key is remembered for 24 hours. That is the window a replay answers inside; past it the key is forgotten, and the same key sent again is a new request that does the work a second time. It is sized for a retry, not for deduplicating a week of pushes.

A key per intent

The SDKs generate a key per call and reuse it across their own retries, which is what makes an SDK retry safe. You can do better by supplying your own.

Derive it from the thing that makes the intent unique. Usually the order and the step.

# The order and the step, not a fresh UUID. A retry of this exact
# intent from anywhere collapses onto the same key.
curl https://api.beaconbox.eu/api/v1/messages \
-H "Authorization: Bearer $BEACONBOX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711-dispatched" \
-d '{"recipient_email": "buyer@example.com",
     "subject": "Your order is on its way",
     "body": "Collected by the courier.",
     "reference": "#A-10294",
     "order_status": "shipped",
     "kind": "updateable"}'

When the connection dies

An error with no response is the case all of this exists for. It is not proof the work did not happen: the request may have been processed and the answer lost on the way back.

The correct response is to retry with the same key. If the first attempt landed, you get its response replayed. If it did not, the work happens now. Either way the customer gets one message.

Both SDKs do this for you, with exponential backoff and full jitter, retrying connection failures, 429 and 5xx, and never retrying a 4xx, because sending the same wrong request again asks the same question.

# Retrying by hand: same key, same body.
for attempt in 1 2 3; do
code=$(curl -s -o /tmp/out.json -w '%{http_code}' \
  https://api.beaconbox.eu/api/v1/messages \
  -H "Authorization: Bearer $BEACONBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4711-dispatched" \
  -d @push.json) && [ "$code" -lt 500 ] && break
sleep $((attempt * 2))
done
cat /tmp/out.json

Batches

A batch takes one key for the whole call. Replaying it replays the whole batch response, including the per-item failures, rather than re-running the items that succeeded. See Batches.