# Idempotency and retries

Source: https://modulify.ai/docs/api/idempotency

Send the Idempotency-Key header so a repeated write gets the first answer back instead of running twice.

Networks drop answers. When a call times out you cannot tell whether it ran, and sending it again could create a second site, queue the same prompt twice or send an email twice. The `Idempotency-Key` header makes a repeat safe.

## How it works

Add a key of your own to a call. Any value of 1 to 255 visible ASCII characters works, and a fresh UUID for each new piece of work is the usual choice.

```bash
curl -X POST https://api.modulify.ai/v1/send_message \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Idempotency-Key: 3f8b6c2e-2a51-4f0e-9a7d-6a4d1c9b2e10" \
  -d '{"projectId":"SITE_ID","prompt":"Add a newsletter sign-up form to the footer."}'
```

Then send exactly the same request again, with the same key, whenever you are not sure the first one arrived:

| What happened to the first call | What the repeat gets |
| --- | --- |
| It succeeded | The first call's answer, without running again, with the header `Idempotency-Replayed: true` |
| It is still running | A `409` saying a request with this key is still running. Wait, then repeat it again |
| It failed, whatever the reason | Nothing is kept, so the repeat runs as a new attempt |
| It never arrived | The repeat runs, as the first attempt |

A key belongs to the token that sent it, so two tokens can never collide. A successful answer is kept for 24 hours. After that the key is forgotten and a call with it runs again.

## Rules

- **Same key, same request.** A key reused with a different method, or with a body that differs in any byte, is refused with a `422`. Use a new key for new work.
- **Only successes are kept.** A call that failed releases its key, so after you fix the cause you can send it again with the same key.
- **Large answers are not replayed.** An answer bigger than 1 MB is not kept. A repeat of that call gets the original status code with `data` set to `null` and a message asking you to read the current state instead, and it does not run again.
- **A bad key is refused.** An empty key, or one longer than 255 characters or with spaces or other characters outside visible ASCII, gets a `400`.
- **Replays are free.** A replayed answer does not count against the token's per-minute budget.

## Which calls need a key

Every method's page says whether it only reads, makes changes, or is destructive. The same three facts are in `GET /v1/tools` and in the [OpenAPI description](https://modulify.ai/docs/api/openapi).

- **Reads** are safe to repeat as they are. Asking twice gives you the current state twice.
- **Changes that set a value** end in the same state however many times they run, such as renaming a site or turning a scheduled job off. A repeat is harmless, though a key still saves the second call.
- **Changes that add something** are the ones to protect: `create_site`, `duplicate_site`, `send_message`, `send_site_email`, `create_backup`, uploads with a unique file name, and anything else that creates a new item each time. Always send these with a key.

When a write fails with a `500` or a dropped connection, repeat it with the same key. If the first attempt had in fact succeeded, you get its answer back. If it had not, it runs now.

## A retry loop

```javascript
import { randomUUID } from 'node:crypto'

const Call = async (method, body, key = randomUUID()) => {
    for (let attempt = 1; attempt <= 5; attempt++) {
        const response = await fetch(`https://api.modulify.ai/v1/${method}`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${process.env.MODULIFY_TOKEN}`,
                'Idempotency-Key': key
            },
            body: JSON.stringify(body)
        })

        if (response.status === 429 || response.status === 409) {
            const wait = Number(response.headers.get('Retry-After') || 2)

            await new Promise(resolve => setTimeout(resolve, wait * 1000))

            continue
        }

        if (response.status >= 500) {
            await new Promise(resolve => setTimeout(resolve, attempt * 2000))

            continue
        }

        return await response.json()
    }

    throw new Error(`${method} did not complete after five attempts`)
}
```

The key is created once and reused on every attempt, which is what makes the loop safe.

## Next

- [Errors](https://modulify.ai/docs/api/errors) for what each status code means.
- [Long-running work](https://modulify.ai/docs/api/long-running-work) for calls that answer before the work is done.