Idempotency and retries
Send the Idempotency-Key header so a repeated write gets the first answer back instead of running twice.
On this page
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.
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
dataset tonulland 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.
- 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
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 for what each status code means.
- Long-running work for calls that answer before the work is done.