> ## Documentation Index
> Fetch the complete documentation index at: https://docs.robase.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry safely — duplicate requests never send duplicate messages.

Network calls fail. Timeouts happen. You retry. Without idempotency, a retried `POST /v1/sms` could send the same message twice.

Robase fixes this with an **`Idempotency-Key`** header: the first successful request wins, and subsequent retries with the same key return the original response without doing the work again.

## How it works

Pass a unique key per logical send attempt:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.robase.dev/v1/sms \
    -H "Authorization: Bearer rb_live_xxx" \
    -H "Idempotency-Key: order-92413-dispatch-sms" \
    -H "Content-Type: application/json" \
    -d '{"to":"+2348012345678","from":"SHUTTLERS","body":"Your ride arrives in 3 min."}'
  ```

  ```typescript Node.js theme={null}
  await pm.sms.send(
    { to: '+2348012345678', from: 'SHUTTLERS', body: 'Your ride arrives in 3 min.' },
    `order-${orderId}-dispatch-sms`  // idempotency key
  );
  ```

  ```python Python theme={null}
  pm.sms.send(
      to="+2348012345678", from_="SHUTTLERS",
      body="Your ride arrives in 3 min.",
      idempotency_key=f"order-{order_id}-dispatch-sms",
  )
  ```

  ```go Go theme={null}
  pm.SMS.Send(ctx, &robase.SendSMSRequest{
      To: "+2348012345678", From: "SHUTTLERS", Body: "Your ride arrives in 3 min.",
  }, robase.WithIdempotencyKey(fmt.Sprintf("order-%d-dispatch-sms", orderID)))
  ```
</CodeGroup>

Replayed responses include an `Idempotent-Replay: true` header so you can tell them apart from fresh requests.

## Rules

<AccordionGroup>
  <Accordion title="Keys are scoped to the project and the endpoint">
    `order-92413-dispatch-sms` on `/v1/sms` is distinct from the same key on `/v1/emails`. And keys from project A never collide with project B.
  </Accordion>

  <Accordion title="Keys live for 24 hours">
    After 24 hours the cached response is evicted. If you retry later than that, you get a fresh attempt — so keep retries inside a reasonable window.
  </Accordion>

  <Accordion title="Failures are retryable, not cached">
    We only cache `2xx` and `4xx` responses. `5xx` errors are not cached — the retry with the same key triggers a fresh attempt.
  </Accordion>

  <Accordion title="Batch rows each need their own key">
    For `POST /v1/sms/batch`, put the per-row key on each element:

    ```json theme={null}
    {
      "from": "SHUTTLERS",
      "messages": [
        { "to": "+2348012345678", "body": "...", "idempotency_key": "order-1-sms" },
        { "to": "+2348087654321", "body": "...", "idempotency_key": "order-2-sms" }
      ]
    }
    ```

    Per-row keys are namespaced separately from the top-level `Idempotency-Key` header.
  </Accordion>
</AccordionGroup>

## What to use as a key

A good idempotency key is **derived from your data**, not random:

* `order-92413-dispatch-sms` ✅
* `user-42-password-reset-2026-04-17` ✅
* `uuid.v4()` ❌ (a fresh UUID per retry is not idempotent — you'll send twice)

If your natural key isn't unique per send attempt, compose it: `user-42-reset-${Date.now()}` bucketed to the minute.
