Docs
Concepts

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.

Preview· P9

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 sendWhat happens
A key Agoo hasn't seen in the last 24 hoursThe 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 finishedAgoo 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 runningconflict (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 keyvalidation_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_error or unavailable are never replayed, so retrying with the same key gets a fresh attempt.
  • GET, PATCH and DELETE don't take a key. Reading changes nothing, and sending the same PATCH or DELETE twice 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:

post-with-retry.ts
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.

On this page