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

# Idempotency

> Retry billed requests safely: the same Idempotency-Key never places a second call or sends a second message.

Some endpoints spend money or reach a real person: they place calls, send WhatsApp messages or run billed analyses.
If your request times out, you cannot tell whether it ran. Retrying blindly could call someone twice.

To make retries safe, these endpoints **require** an `Idempotency-Key` header when called with an API key:

```bash theme={null}
curl -X POST https://api.pineyard.ai/calls/test \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: 7d0f3c1e-2b5a-4c8e-9f61-0a1b2c3d4e5f" \
  -H "Content-Type: application/json" \
  -d '{"agentId":"00000000-0000-4000-8000-000000000001","toNumber":"+15555550100","fromPhoneNumberId":"00000000-0000-4000-8000-000000000002"}'
```

## Rules

* Use a **new unique value per action**, for example a UUID. Up to 200 printable ASCII characters.
* **Reuse a key only to retry the same request.** The request is identified by method, path, JSON body and any
  uploaded files (query strings are not part of it).
* Keys are remembered **per API key for 24 hours** after the first request finishes.

## What happens on a retry

| Situation | Response |
| - | - |
| First request with this key | Runs normally. The response is stored. |
| Same key, same request, first one finished | The stored response is returned again with header `Idempotent-Replayed: true`. Nothing runs twice. |
| Same key while the first request is still running | `409 idempotency_in_progress` with `Retry-After: 1`. Retry shortly. |
| Same key, **different** request | `422 idempotency_key_reused`. Use a new key. |
| No `Idempotency-Key` header | `400 idempotency_key_required` |
| Header too long or not printable ASCII | `400 idempotency_key_invalid` |
| The idempotency store did not answer in time | `503 idempotency_unavailable`. Nothing ran; retry with the same key. |

### Which responses are remembered

* `402` (insufficient wallet balance), `429` (rate limited) and `503` are **not** stored. Nothing happened, so
  retry with the **same** key after topping up or waiting.
* **Every other response is stored, including `5xx` errors.** A server error can occur after the call was placed or
  the message sent, so a same-key retry returns the stored error instead of doing it twice. Check what happened
  (for example, list recent calls or messages), then use a **new** key if you still want to go ahead.
* Authentication and scope failures (`401`, and `403` from the scope check) happen before the key is checked, so
  they are not stored.
* A `400` validation error **is** stored. After fixing the body, send it with a **new** key: the corrected
  request is a different request, so reusing the old key returns `422 idempotency_key_reused`.

## Endpoints that require it

| Endpoint | Scope |
| - | - |
| `POST /agents/{id}/duplicate` | `agents.edit` |
| `POST /agents/{id}/test-call` | `calls.test` |
| `POST /agents/{id}/web-session` | `calls.test` |
| `POST /calls/test` | `calls.test` |
| `POST /campaigns/{id}/reengage` | `campaigns.edit` |
| `POST /campaigns/{id}/finish-interrupted` | `campaigns.edit` |
| `POST /campaigns/{id}/remessage` | `campaigns.edit` |
| `POST /knowledge-bases/{id}/documents` | `chatbots.edit` |
| `POST /speech/calls` | `speech.upload` |
| `POST /speech/calls/bulk` | `speech.upload` |
| `POST /whatsapp/conversations/{id}/messages` | `whatsapp.send` |
| `POST /whatsapp/conversations/{id}/media` | `whatsapp.send` |
| `POST /whatsapp/test-send` | `whatsapp.send` |
| `POST /whatsapp/messages/{id}/retry` | `whatsapp.send` |
| `POST /workflows/{id}/runs` | `workflows.run` |

Other endpoints ignore the header.

## Pushing leads to `/v1`

`POST /v1/campaigns/{campaignId}/leads` and `POST /v1/campaigns/{campaignId}/leads/batch` do not use the
`Idempotency-Key` header. Instead, each lead carries its own `idempotencyKey` in the request body; pushing the same
key again is reported as a duplicate and nobody is called twice. In a batch, every result echoes the
`idempotencyKey` you sent. See [Run a campaign end to end](/guides/run-a-campaign#push-leads-with-the-public-api).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.