Skip to content

Errors and rate limits ​

The error shape ​

Every error has the same envelope. Branch on error.code, never on message: messages can change and some are in Indonesian.

400 Bad Request

json
{
    "success": false,
    "error": {
        "code": "INVALID_BODY",
        "message": "Validasi gagal",
        "details": {
            "formErrors": [],
            "fieldErrors": {
                "amount": [
                    "Too small: expected number to be >=1000"
                ]
            }
        }
    }
}

Validation errors (INVALID_BODY) add error.details.fieldErrors, keyed by field name. Some errors add a field of their own, for example requires_captcha on a failed sign-in.

What to do with each status ​

StatusMeaningRetry?
400The request is wrong (INVALID_BODY, INVALID_QUERY, a bad value)No. Fix the request
401No valid session or keyNo. Sign in again or fix the key
402Not enough balanceNo
403Not allowed (FORBIDDEN, CSRF_FAILED, KEY_NOT_ALLOWED, SCOPE_REQUIRED)No
404Not found, or not visible to youNo
409The state does not allow it, or someone changed it firstRead the resource again, then decide
422The request is well formed but refused (signed links, idempotency conflicts)No
423Sign-in locked after failed attemptsLater
429Rate limitedYes, after Retry-After
5xxServer or gateway problemYes, with the same Idempotency-Key and backoff

Real examples:

401 Unauthorized

json
{
    "success": false,
    "error": {
        "code": "UNAUTHORIZED",
        "message": "No session."
    }
}

403 Forbidden

json
{
    "success": false,
    "error": {
        "code": "FORBIDDEN",
        "message": "Your workspace role does not allow this action"
    }
}

409 Conflict

json
{
    "success": false,
    "error": {
        "code": "NOT_DRAFT",
        "message": "Only draft deals can be edited."
    }
}

Rate limits ​

BudgetLimit
Signed-in calls (session or API key)300 requests a minute per user, across all routes
Calls without valid credentials100 requests a minute per IP address
Sign-inLocks after repeated failures for the same IP and email
Some enterprise routesTheir own budget per user, from 5 to 60 a minute. See Enterprise overview
Tracking refreshOnce per escrow every 5 minutes

Every answer carries the current budget. This is a call without credentials:

http
x-ratelimit-limit: 100
x-ratelimit-remaining: 99
x-ratelimit-reset: 60

Over the limit you get 429 with a Retry-After header in seconds. This is the 101st call in a minute from one IP address without credentials, captured with Retry-After: 60:

429 Too Many Requests

json
{
    "success": false,
    "requestId": "req-hw",
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Try again in 60s."
    }
}

Some routes have their own short limit with their own message, for example refreshing tracking twice within 5 minutes:

429 Too Many Requests

json
{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Tunggu sebentar sebelum memuat ulang status lagi."
    }
}

Handling them in code ​

js
async function call(url, init = {}, attempts = 4) {
    for (let i = 0; i < attempts; i += 1) {
        const res = await fetch(url, init);
        if (res.status === 429) {
            const wait = Number(res.headers.get('retry-after') ?? 1);
            await new Promise((r) => setTimeout(r, wait * 1000));
            continue;
        }
        if (res.status >= 500) {
            await new Promise((r) => setTimeout(r, 2 ** i * 500));
            continue;
        }
        const body = await res.json();
        if (!body.success) {
            const err = new Error(body.error.message);
            err.code = body.error.code;
            err.status = res.status;
            throw err;
        }
        return body.data;
    }
    throw new Error('Gave up after retries');
}

Retry writes only with an Idempotency-Key, so a retry never acts twice. See Idempotency and retries.

Staying under the limits ​

  • Poll slowly: every 5 seconds for a payment you are waiting on, every 15 to 60 seconds for the enterprise event feed.
  • Cache data that rarely changes, such as payment methods and workspace settings.
  • The budget belongs to the user, so every key and session of one user shares it.
  • Do not load test against production.

Next steps ​

Escrow, payment services and B2B deals for Indonesian businesses.