Rate Limits & Idempotency
Plan-based rate limits, X-RateLimit headers, and Idempotency-Key semantics that make retries safe.
#Rate limits
Rate limits apply per account based on plan, on both JWT and API-key routes alike:
| Plan | Limit |
|---|---|
| Free | 60 requests / minute |
| Pro | 600 requests / minute |
| Enterprise | 6,000 requests / minute |
Public auth endpoints (register, login, etc.) are separately capped at 30 req/min per IP. Every response includes X-RateLimit-Limit and X-RateLimit-Remaining headers — surface them in your monitoring and back off before you hit the ceiling.
{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests"
}
}Official SDKs auto-retry on 429 and 5xx with exponential backoff — min(0.5 × 2^(attempt−1), 8.0) seconds, max 3 retries — and expose retry_after on RateLimitError so you can honor it manually too.
#Idempotency
Pass an Idempotency-Key header (any string, ≤128 chars) on POST /v1/sms/, POST /v1/sms/bulk, and campaign creation to make retries safe:
curl -X POST https://api.sendafrica.online/v1/sms/ \
-H "X-API-Key: $SENDAFRICA_API_KEY" \
-H "Idempotency-Key: order-1234-sms" \
-H "Content-Type: application/json" \
-d '{ "to": "0712345678", "message": "Your order shipped!" }'| Scenario | Behavior |
|---|---|
| First request succeeds | Response cached for 24 h — retries replay it instead of double-charging |
| Replayed response | Identical body plus Idempotent-Replay: true response header |
| Original request failed | Nothing cached — a retry genuinely tries again |
| Same key while first request still in flight | 409 request_in_progress |
Derive keys from business IDs
Use deterministic keys like order-{id}-confirmation rather than UUIDs — that way a retry after a timeout dedupes correctly even across processes.
#Credit ledger guarantees
Credits are charged per SMS part and tracked in an append-only ledger. Balance is derived from the latest completed transaction row — never stored in a separate column — and cached in Redis for 30 s. Every mutation goes through one atomic path: begin transaction → check idempotency → lock latest row FOR UPDATE → compute new balance (rejecting insufficient funds) → insert + commit. Concurrent sends can never overdraw an account.