# Retry safely

> Idempotency keys, so a dropped response never makes two etches.

A request that times out tells you nothing about whether it landed. Etches can't be deleted, and two attempts would make two etches with different salts and times — so a dropped response could otherwise cost you a visible retraction to clean up.

## The header

Send an `Idempotency-Key` on `POST /etches` and `POST /challenges`. A retry carrying the same key returns the original result with `Idempotent-Replay: true`, writes nothing, and spends no allowance.

curl:

```bash
curl https://test.etch.so/api/v1/etches \
  -H "Authorization: Bearer $ETCH_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "content": "BTC closes above $150,000 before the end of Q4 2026.",
    "description": "Bitcoin price by the end of Q4",
    "category_id": "crypto",
    "mode": "delayed",
    "reveal_at": "2026-12-31T23:59:00.000Z"
  }'
```

JavaScript:

```js
// Make an etch.
const res = await fetch('https://test.etch.so/api/v1/etches', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    authorization: `Bearer ${API_KEY}`,
    // Makes a retry safe. Generate it once per etch, keep it across retries:
    // the same key returns the original etch instead of a second one.
    'idempotency-key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    content: 'BTC closes above $150,000 before the end of Q4 2026.',
    description: 'Bitcoin price by the end of Q4',   // public immediately, even while sealed
    category_id: 'crypto',
    mode: 'delayed',
    reveal_at: '2026-12-31T23:59:00.000Z',
  }),
})
```

Python:

```python
# Make an etch. The server assigns the time and the salt.
import json, os, urllib.request, uuid

req = urllib.request.Request(
    "https://test.etch.so/api/v1/etches",
    data=json.dumps({
        "content": "BTC closes above $150,000 before the end of Q4 2026.",
        "description": "Bitcoin price by the end of Q4",   # public immediately, even while sealed
        "category_id": "crypto",
        "mode": "delayed",
        "reveal_at": "2026-12-31T23:59:00.000Z",
    }).encode(),
    headers={
        "content-type": "application/json",
        "authorization": f"Bearer {os.environ['ETCH_API_KEY']}",
        # Makes a retry safe: generate once per etch, keep it across retries.
        "idempotency-key": str(uuid.uuid4()),
    },
)
etch = json.load(urllib.request.urlopen(req))["etch"]
print(etch["id"], etch["status"])
```
## The rules

| | |
| --- | --- |
| Generate it | When you decide to write, and save it **before** the first attempt. A fresh key per attempt protects against nothing |
| Reuse it | On every retry of that same etch, for as long as you keep retrying |
| Change it | For a new etch. A key reused with a different body is refused with `409`, rather than quietly returning the wrong result |
| Keep it | Keys are kept for the life of the record and never expire |
