Concepts
Rate limits and quotas
Each key’s per-minute limit and monthly quota, the RateLimit-* headers, what to do when you get a 429, and request size limits.
On this page
Every key has two limits, configured when it’s issued:
| Limit | Default value | When exceeded |
|---|---|---|
| Per minute | 120 requests per minute | 429 with Retry-After until the window frees up |
| Monthly quota (optional) | No quota, unless your contract sets one | Warnings when thresholds are crossed and, depending on the contract, 429 until the next period |
If your volume needs more, ask for it during onboarding or from support: the limit is adjusted per key.
Headers #
Responses report the state of your current window:
RateLimit-Limit: 120
RateLimit-Remaining: 87
RateLimit-Reset: 23
RateLimit-Policy: 120;w=60| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the window. |
RateLimit-Remaining | How many you have left. |
RateLimit-Reset | Seconds until the window resets. |
RateLimit-Policy | Policy applied (quota and window w= in seconds). |
Retry-After | Only on 429: seconds to wait before retrying. |
A 429 arrives as problem+json with type: https://errors.compense.co/gateway/rate-limited.
How to respond to a 429 #
The SDKs retry 429s on reads (and on writes if you enabled retry_writes),
honoring Retry-After (Retries). If the attempts run out,
you get the exception with the wait time:
import time
from compense_gateway import RateLimitError
try:
page = client.credits.list_applications(status="RECEIVED")
except RateLimitError as e:
time.sleep(e.retry_after or 60) # or requeue the jobimport { RateLimitError } from "@compense/sdk";
try {
await client.credits.listApplications({ status: "RECEIVED" });
} catch (err) {
if (!(err instanceof RateLimitError)) throw err;
await new Promise((r) => setTimeout(r, (err.retryAfter ?? 60) * 1000)); // retryAfter in seconds
}use Compense\Sdk\Exception\RateLimitException;
try {
$client->credits->listApplications(status: 'RECEIVED');
} catch (RateLimitException $e) {
sleep((int) ceil($e->retryAfter ?? 60));
}try
{
await client.Credits.ListApplicationsAsync(status: "RECEIVED");
}
catch (RateLimitException e)
{
await Task.Delay(e.RetryAfter ?? TimeSpan.FromSeconds(60));
}_, err := client.Credits.ListApplications(ctx, &compense.ListApplicationsParams{Status: "RECEIVED"})
if compense.IsRateLimited(err) {
var apiErr *compense.APIError
errors.As(err, &apiErr)
wait := time.Minute
if apiErr.RetryAfter != nil {
wait = *apiErr.RetryAfter
}
time.Sleep(wait)
}curl -s -D - -o /dev/null "https://sandbox.dev.compense.co/gw/v1/gateway/credits/applications/?status=RECEIVED" \
-H "X-API-Key: $COMPENSE_API_KEY" | grep -iE '^(ratelimit|retry-after)'Design to stay under the limit #
- Batch bulk processes. To load affiliates use
affiliates.cross(up to 1,000 per request) or the file upload, not one call per affiliate. - Webhooks instead of polling. Don’t check an operation’s status in a loop: subscribe to the event (Webhooks).
- Bounded concurrency. Limit how many workers call the gateway at once and use a queue with deferred retries.
- Cache stable reads (biller catalog, top-up carriers, health catalog): they change rarely, so don’t request them on every transaction.
Size limits #
| What | Limit | When exceeded |
|---|---|---|
| JSON request body | 1 MB | 413 |
Affiliates file (multipart/form-data) | 25 MB | 413 |
affiliates.cross batch | 1,000 records per request | 422 |
page_size / limit | 100 | Capped at 100 |