Rate Limits, Retries, and Production Reliability
The business problem
A call that works once in a terminal and a call that survives production traffic are different engineering problems. This guide covers what changes between the two: what happens when you exceed quota, when and how to retry, which requests are safe to retry blindly, and how to trust an inbound webhook. Skipping this is how a rate limit turns into an outage, or a retried POST turns into a duplicate side effect.
What AfriHex returns
Every response carries the headers you need to behave correctly under load, whether it succeeded or not:
| Header | Present on | Meaning |
|---|---|---|
X-RateLimit-Limit | Every response | Max requests allowed in the rolling 24h window |
X-RateLimit-Remaining | Every response | Requests left in the window |
X-RateLimit-Reset | Every response | Unix timestamp when the window resets |
Retry-After | 429 responses | Seconds to wait before retrying |
X-Request-ID | Every response | Echoes meta.request_id, for tracing a specific call |
Before you begin
- Know your tier's quota (Free: 100/24h, Basic: 5,000, Pro: 20,000, Enterprise: 100,000+ — see Rate Limits) before you design a polling loop against it.
- Decide, per endpoint you call, whether a retry is safe — see Idempotency below, since this isn't uniform across the API.
- If you're receiving webhooks, generate a real per-endpoint
secretat configure-time — see Webhook verification.
Copyable request
A 429 looks like this — the shape to build your backoff logic against:
curl -i "https://api.afrihex.com/v2/hexcode?lat=5.6037&lng=-0.1870&res=7" \
-H "X-API-Key: $AFRIHEX_API_KEY"
Full example response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1788358800
Retry-After: 3421
{
"success": false,
"error": { "code": "RATE_LIMITED", "message": "rate limit exceeded for this key" },
"meta": { "request_id": "…" }
}
Field-by-field explanation
Retry-After— the number to actually sleep on. It's simpler and more current than computingX-RateLimit-Reset - now(); prefer it when both are present.X-RateLimit-Remaining: 0doesn't mean you're blocked forever — it resets atX-RateLimit-Reset, a rolling 24h window, not a fixed daily midnight cutoff.error.code: RATE_LIMITEDvs.error.code: TOO_MANY(seen on async job submission) — both are quota problems, but the second is a plan limit (e.g. rows per job), not a rate limit; check Rate Limits vs. your plan's row caps separately.
Idempotency
Retrying a failed request is only safe if retrying can't duplicate a side effect. AfriHex's idempotency story isn't uniform — check which case you're in:
| Case | Safe to retry blindly? |
|---|---|
GET requests (encode, lookup, reverse, decode, …) | Yes — read-only, no side effect |
POST /v2/bulk/validate, POST /v2/jobs | Yes, if you send an Idempotency-Key header — the same key returns the cached job instead of creating a duplicate (cached 24h) |
Any other POST (e.g. kyc/verify, consent/grant, service-area, webhooks/configure) | Not guaranteed — no documented idempotency key. Dedupe yourself: check for an existing verification_id/resource before creating another, or make your own request unique on a field your system already tracks (e.g. one kyc/verify call per customer_id per day) |
Error and edge cases
- A
5xxon a non-idempotentPOST. You genuinely don't know if it landed. Query for the resource it would have created (e.g.GET /v2/certificates/{id}for a KYC verification) before blindly re-sending. - Repeated
429s even after waiting forRetry-After. You're sending faster than your quota resets on average — this is a capacity problem, not a timing bug; upgrade tier or reduce call volume (cache more, batch more — see Rate Limits — best practices). - A webhook delivery your handler already processed. Expected — AfriHex
retries with exponential backoff until it gets a
2xx, so the sameX-Webhook-Deliverycan arrive more than once. Dedupe on that header. - A webhook signature that doesn't verify. Don't process the event —
check you're hashing the raw request body (not a re-serialized/parsed
version of it) against the
secretfrom configure-time.
Webhook verification
Two signature headers exist; verify at least the first, adopt the second for real replay protection:
X-Webhook-Signature—HMAC_SHA256(secret, raw_body). Proves the payload wasn't tampered with, but a captured valid signature never expires — it can be replayed indefinitely.X-Webhook-Signature-V2— bindsX-Webhook-Timestampinto the signed material (HMAC_SHA256(secret, "{timestamp}." + raw_body)); reject anything more than 5 minutes stale regardless of whether it verifies. This is the actual replay defense.
Full verification code and the timing-safe comparison to use: Webhooks — Verify the signature and Replay protection.
Production checklist
- Exponential backoff on
429and5xx, seeded fromRetry-Afterwhen present, capped with jitter — not a fixed retry interval. - Every retryable call is either naturally idempotent (
GET) or carries anIdempotency-Key(bulk/job endpoints) or your own dedupe check (everything else). - Webhook handlers verify
X-Webhook-Signature-V2with a constant-time comparison and reject stale timestamps, not just a raw string match. - Webhook handlers return
2xximmediately and process asynchronously — a slow handler causes AfriHex to retry, multiplying load. -
meta.request_id(andX-Webhook-Deliveryfor webhooks) is logged on every call, so a support ticket can reference the exact request. - Usage is monitored against quota proactively (
GET /v2/usage), not discovered via production429s.
Related endpoints / next guide
- Your First AfriHex Request — the request/response basics this guide builds on.
- Rate Limits — full tier table and additional throttles.
- Webhooks — event types, delivery format, and retry history.