Idempotency
Send an Idempotency-Key with every POST so a retry after a timeout or a dropped connection never invites a visitor or books a slot twice.
This is a published preview. Names and fields may still change before general availability; changes will be listed in the changelog.
Networks fail at the worst moment. A request to invite a visitor can reach Agoo, succeed, and then lose its response on the way back when mobile data drops or the power goes. Your code sees a timeout and doesn't know whether the visitor was invited. If it simply retries, the visitor might get two passes.
Idempotency keys solve this. You give each operation a unique key; if you send the same request with the same key again, Agoo returns the result of the first attempt instead of doing the work twice.
How it works
Every POST request needs an Idempotency-Key header containing a UUID that you generate:
curl https://api.agoo.ardent.africa/v1/visits \
-H "Authorization: Bearer $AGOO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f6b2a1e-8c4d-4f7a-9b2e-5d1c0a7e9f64" \
-d '{ "site_id": "site_01kjpt3yw0fz0v414608h9x65s", "host_id": "person_01kjsesxm0e4zbyvkr7bcfxbfg", "visitor": { "name": "Ama Owusu", "phone": "+233241234567" }, "expected_at": "2026-10-14T09:30:00Z" }'| What you send | What happens |
|---|---|
| A key Agoo hasn't seen in the last 24 hours | The request runs normally, and Agoo stores its status code and body against the key |
| The same key with the same request, after the first one finished | Agoo returns the stored status code and body. Nothing runs again; no event fires again |
| The same key with the same request, while the first is still running | conflict (409). Wait a moment and retry with the same key |
| The same key with a different request (another method, path or body) | idempotency_key_reused (422) |
A POST with no key | validation_failed (422) at header.Idempotency-Key |
Some details:
- Keys last 24 hours from the first request. After that, the same key starts a new operation.
- Keys are scoped to your organisation and mode. A test-mode key never collides with a live one, and separate integrations can't collide with each other as long as each generates random UUIDs.
- Errors are stored too. If the first attempt failed validation, a retry with the same key returns the same
validation_failed. Fix the request and send it with a new key. - Throttling and server errors aren't stored. Responses with
rate_limited,internal_errororunavailableare never replayed, so retrying with the same key gets a fresh attempt. GET,PATCHandDELETEdon't take a key. Reading changes nothing, and sending the samePATCHorDELETEtwice leaves the object in the same state.
Generate and keep keys
Use a random UUID (version 4 or 7) for each operation, not each HTTP attempt: every retry of one operation must reuse its key.
IDEMPOTENCY_KEY=$(uuidgen)If your process might crash between sending a request and recording the result, save the key before you send, next to the record it belongs to. For example, store it on the CRM meeting that triggers an invitation. After a restart you'll retry with the same key, and Agoo will hand back the visit it already created.
Retry safely
This helper retries network errors, rate_limited, internal_error and unavailable with the same key, and gives up on anything else:
const RETRYABLE = new Set([429, 500, 503])
export async function postWithRetry(path: string, body: unknown, idempotencyKey = crypto.randomUUID()) {
for (let attempt = 1; ; attempt++) {
let res: Response | undefined
try {
res = await fetch(`https://api.agoo.ardent.africa/v1${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AGOO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
})
} catch (err) {
if (attempt >= 5) throw err // network error or timeout: the request may or may not have run
}
if (res && !RETRYABLE.has(res.status)) {
if (!res.ok) throw new Error(`Agoo API ${res.status}: ${await res.text()}`)
return res.json()
}
if (attempt >= 5) throw new Error(`Agoo API still failing after ${attempt} attempts (status ${res?.status})`)
const retryAfter = Number(res?.headers.get("retry-after"))
const backoff = Math.min(30, 2 ** attempt) * (0.5 + Math.random() / 2)
await new Promise((r) => setTimeout(r, 1000 * (retryAfter > 0 ? retryAfter : backoff)))
}
}Webhooks are different
Idempotency keys protect requests you send to Agoo. For events Agoo sends you, de-duplicate on the webhook-id header instead; see handling events idempotently.