Modulify

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 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.

  • 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