Skip to content

Errors

Every error response from the RekberPay API follows the same shape:

json
{
    "success": false,
    "error": {
        "code": "INVALID_SIGNATURE",
        "message": "Signature does not match canonical string."
    }
}

The HTTP status code tells you the category. The code field gives you something stable to switch on.

HTTP status categories

StatusCategoryRetry?
400Validation / bad inputNo — fix the input
401UnauthenticatedNo — log in / get credentials
403Forbidden / signature invalidNo — fix permissions / re-sign
404Not foundNo
409Conflict (duplicate, idempotency mismatch)Maybe — see specific code
410Gone (expired)No
413Payload too largeNo — shrink the body
429Rate limitedYes — back off
500Internal errorYes — retry with same idempotency key
502 / 503Upstream gateway downYes — retry with backoff

Error codes

Authentication & signing

CodeStatusCauseRecovery
INVALID_SIGNATURE403sig doesn't match the canonical stringRe-sign with correct field order, encoding, secret
LINK_EXPIRED400expires is in the pastGenerate a fresh link with future expires
INVALID_VERSION400v is not 1Use v: 1
MISSING_FIELD400A required field is absentCheck required fields per endpoint
UNAUTHORIZED401No session, expired session, or no API keyRe-authenticate

Idempotency

CodeStatusCauseRecovery
IDEMPOTENCY_KEY_INVALID400Header value isn't a UUID v4Use randomUUID() / uuid.uuid4()
IDEMPOTENCY_PARAMS_MISMATCH409Same key, different request bodyGenerate a fresh key for the new operation

Validation

CodeStatusCauseRecovery
INVALID_BODY400Body failed Zod schema validationCheck required fields and types
INVALID_AMOUNT400amount < 10,000 or > 100,000,000 IDRClamp to allowed range
INVALID_METHOD400Payment method not recognizedUse one returned by /api/gateways/available-methods
METHOD_DISABLED400Method exists but currently disabledPick a different method
INVALID_USERNAME400Username doesn't match ^[a-z0-9_.]{3,30}$Lowercase alphanumeric + underscore/dot
WEAK_PASSWORD400Password fails strength rules8+ chars, mixed case, digit
INVALID_ACTION400Admin escrow action isn't in the allow-listUse update-status, admin_force_release, cancel, release, refund, resolve_release, resolve_refund, resolve_hold. For dispute rulings use POST /admin/disputes/:id/resolve
MISSING_FIELDS400Required action or reason is absent/emptySend a non-empty reason with every admin escrow action
REASON_TOO_LONG400Admin action reason exceeds 2000 charsTrim the reason to ≤ 2000 characters

Resource state

CodeStatusCauseRecovery
NOT_FOUND404Resource ID doesn't existVerify the ID
SELLER_NOT_FOUND404seller UUID isn't a real sellerConfirm with support
INVALID_STATE409Escrow can't transition (e.g., paying an already-paid escrow)Refresh and check current status
INVALID_TRANSITION400State machine forbids this action for this roleSee escrow lifecycle
DUPLICATE_REF409ref already used by this sellerUse a new ref, or fetch the existing escrow

Authorization

CodeStatusCauseRecovery
FORBIDDEN403Authenticated but not allowedCheck role / ownership
NOT_A_PARTY403Trying to read/post in an escrow you're not a party toBuyer or seller only
NOT_BUYER403Buyer-only action attempted by seller/adminRight user, wrong role
SUSPENDED403User account is suspendedContact support

Rate limiting & body size

CodeStatusCauseRecovery
RATE_LIMITED429Too many requests in 1 minuteHonor Retry-After, back off
PAYLOAD_TOO_LARGE413Request body > 1 MiBTrim the body — chat attachments use /api/upload

Server-side

CodeStatusCauseRecovery
INTERNAL_ERROR500Unhandled server errorRetry with same idempotency key
GATEWAY_TIMEOUT504Payment gateway didn't respond in timeRetry; payment may still complete

Reading error responses

Every error response is JSON. Treat anything else as a network/proxy issue, not a RekberPay error.

js
const res = await fetch(url, options);
if (!res.ok) {
    const ct = res.headers.get('content-type') ?? '';
    if (!ct.includes('application/json')) {
        throw new Error(`Network error: ${res.status} ${res.statusText}`);
    }
    const { error } = await res.json();
    throw new ApiError(error.code, error.message, res.status);
}

Recovery patterns

Idempotent retry on 5xx / network failure

js
async function withRetry(fn, max = 5) {
    let attempt = 0;
    while (attempt < max) {
        try {
            return await fn();
        } catch (err) {
            if (err.status && err.status < 500) throw err; // 4xx — don't retry
            if (attempt === max - 1) throw err;
            await sleep(1000 * Math.pow(2, attempt++));
        }
    }
}

Backoff on 429

The response includes a Retry-After header (seconds):

js
if (res.status === 429) {
    const wait = Number(res.headers.get('Retry-After') ?? 60);
    await sleep(wait * 1000);
    return retry();
}

Refresh on INVALID_STATE

js
if (err.code === 'INVALID_STATE') {
    // Fetch the latest state and decide what to do
    const { data } = await fetch(`/api/escrows/${id}/payment-status`).then(r => r.json());
    if (data.status === 'paid') {
        // Already paid — treat as success
        return;
    }
    throw err;
}

Reporting bugs

If you hit a 500 you can't explain, email [email protected] with:

  • escrow_id (or other resource ID)
  • Approximate timestamp (UTC)
  • Request body (redact secrets)
  • Response body

We log everything server-side and can usually identify the cause within minutes.

See also

Released under the proprietary RekberPay license. Built for Indonesian merchants.