Skip to main content

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:

HeaderPresent onMeaning
X-RateLimit-LimitEvery responseMax requests allowed in the rolling 24h window
X-RateLimit-RemainingEvery responseRequests left in the window
X-RateLimit-ResetEvery responseUnix timestamp when the window resets
Retry-After429 responsesSeconds to wait before retrying
X-Request-IDEvery responseEchoes 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 secret at 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 computing X-RateLimit-Reset - now(); prefer it when both are present.
  • X-RateLimit-Remaining: 0 doesn't mean you're blocked forever — it resets at X-RateLimit-Reset, a rolling 24h window, not a fixed daily midnight cutoff.
  • error.code: RATE_LIMITED vs. 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:

CaseSafe to retry blindly?
GET requests (encode, lookup, reverse, decode, …)Yes — read-only, no side effect
POST /v2/bulk/validate, POST /v2/jobsYes, 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 5xx on a non-idempotent POST. 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 for Retry-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 same X-Webhook-Delivery can 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 secret from 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 — binds X-Webhook-Timestamp into 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 429 and 5xx, seeded from Retry-After when present, capped with jitter — not a fixed retry interval.
  • Every retryable call is either naturally idempotent (GET) or carries an Idempotency-Key (bulk/job endpoints) or your own dedupe check (everything else).
  • Webhook handlers verify X-Webhook-Signature-V2 with a constant-time comparison and reject stale timestamps, not just a raw string match.
  • Webhook handlers return 2xx immediately and process asynchronously — a slow handler causes AfriHex to retry, multiplying load.
  • meta.request_id (and X-Webhook-Delivery for 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 production 429s.