Retries and idempotency

Retry a send safely: the request-level Idempotency-Key header, and per-message idempotency keys that let a retried batch be regrouped freely.

Network failures and timeouts mean you will sometimes retry a POST /api/v1/messages call without knowing whether the first attempt went through. Beep gives you two tools so a retry never sends a message twice.

ToolBinds toUse it when
Idempotency-Key headerthe whole request bodyyou will retry the exact same request
messages[].idempotency_keyone messageyou want to retry with a different grouping, or resume a batch part-way

Both are scoped to your workspace and expire after 24 hours. A key reused after that window sends again.

The request header

Send an Idempotency-Key header (1–255 characters) with the request. Repeating the same request with the same key returns the original response, including the original message IDs, without sending again. Reusing the key with a different body returns 409 idempotency_conflict.

Because the header binds to the entire body, a retry that regroups messages cannot reuse it. That is what per-message keys are for.

Per-message keys

Give each message its own idempotency_key:

{
  "messages": [
    { "to": "+15555550100", "body": "Hi Ada", "idempotency_key": "order-9281" },
    { "to": "+15555550101", "body": "Hi Ben", "idempotency_key": "order-9282" }
  ]
}

A key binds to that message's content plus the request-level from, reply_destination_id and status_callback. Regrouping across requests changes nothing; changing what a message says or where it goes under the same key is a conflict.

On any later request, in any grouping, each keyed item resolves on its own:

The key was...Result for that item
never seenSent normally. deduplicated: false.
completed with the same contentNot sent again. The ack carries the original id and deduplicated: true.
used with different content, from or routingstatus: "rejected", id: null, error.code: "idempotency_conflict". The rest of the batch still sends.
held by a request still in flightstatus: "rejected", id: null, error.code: "idempotency_in_progress". Retry that item shortly.
part of an earlier attempt that failed part-wayRecovered safely: replayed if the message was persisted, sent otherwise. Never sent twice.

The batch response counts accepted and rejected beside total, and every ack carries deduplicated, so a retry loop is one pass over items in request order:

{
  "job_id": "…",
  "status": "processing",
  "total": 2,
  "accepted": 1,
  "rejected": 1,
  "items": [
    {
      "id": "a1b2…",
      "to": "+15555550100",
      "status": "queued",
      "deduplicated": true,
      "…": "…"
    },
    {
      "id": null,
      "to": "+15555550101",
      "status": "rejected",
      "deduplicated": false,
      "error": { "code": "idempotency_conflict", "message": "…" },
      "…": "…"
    }
  ]
}

A rejected item never fails the request; the response is still 202. Two items in one request may not share a key (400). On the single-message form a conflicting key returns 409 idempotency_conflict for the request, since there is no per-item result to attach it to.

Resuming a batch that failed part-way

If a batch request errors after some items were accepted, retry it with the same per-message keys. Items that were persisted come back as deduplicated: true; the rest send. You do not need to work out where it stopped.

Both together

You can send both. The header is checked first: a header replay returns the stored response and per-message keys are not evaluated; a header conflict is a 409 before any message key is touched. On a fresh header claim, each keyed item then resolves as above.

Related


Did this page help you?