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
| Status | Category | Retry? |
|---|---|---|
400 | Validation / bad input | No — fix the input |
401 | Unauthenticated | No — log in / get credentials |
403 | Forbidden / signature invalid | No — fix permissions / re-sign |
404 | Not found | No |
409 | Conflict (duplicate, idempotency mismatch) | Maybe — see specific code |
410 | Gone (expired) | No |
413 | Payload too large | No — shrink the body |
429 | Rate limited | Yes — back off |
500 | Internal error | Yes — retry with same idempotency key |
502 / 503 | Upstream gateway down | Yes — retry with backoff |
Error codes
Authentication & signing
| Code | Status | Cause | Recovery |
|---|---|---|---|
INVALID_SIGNATURE | 403 | sig doesn't match the canonical string | Re-sign with correct field order, encoding, secret |
LINK_EXPIRED | 400 | expires is in the past | Generate a fresh link with future expires |
INVALID_VERSION | 400 | v is not 1 | Use v: 1 |
MISSING_FIELD | 400 | A required field is absent | Check required fields per endpoint |
UNAUTHORIZED | 401 | No session, expired session, or no API key | Re-authenticate |
Idempotency
| Code | Status | Cause | Recovery |
|---|---|---|---|
IDEMPOTENCY_KEY_INVALID | 400 | Header value isn't a UUID v4 | Use randomUUID() / uuid.uuid4() |
IDEMPOTENCY_PARAMS_MISMATCH | 409 | Same key, different request body | Generate a fresh key for the new operation |
Validation
| Code | Status | Cause | Recovery |
|---|---|---|---|
INVALID_BODY | 400 | Body failed Zod schema validation | Check required fields and types |
INVALID_AMOUNT | 400 | amount < 10,000 or > 100,000,000 IDR | Clamp to allowed range |
INVALID_METHOD | 400 | Payment method not recognized | Use one returned by /api/gateways/available-methods |
METHOD_DISABLED | 400 | Method exists but currently disabled | Pick a different method |
INVALID_USERNAME | 400 | Username doesn't match ^[a-z0-9_.]{3,30}$ | Lowercase alphanumeric + underscore/dot |
WEAK_PASSWORD | 400 | Password fails strength rules | 8+ chars, mixed case, digit |
INVALID_ACTION | 400 | Admin escrow action isn't in the allow-list | Use update-status, admin_force_release, cancel, release, refund, resolve_release, resolve_refund, resolve_hold. For dispute rulings use POST /admin/disputes/:id/resolve |
MISSING_FIELDS | 400 | Required action or reason is absent/empty | Send a non-empty reason with every admin escrow action |
REASON_TOO_LONG | 400 | Admin action reason exceeds 2000 chars | Trim the reason to ≤ 2000 characters |
Resource state
| Code | Status | Cause | Recovery |
|---|---|---|---|
NOT_FOUND | 404 | Resource ID doesn't exist | Verify the ID |
SELLER_NOT_FOUND | 404 | seller UUID isn't a real seller | Confirm with support |
INVALID_STATE | 409 | Escrow can't transition (e.g., paying an already-paid escrow) | Refresh and check current status |
INVALID_TRANSITION | 400 | State machine forbids this action for this role | See escrow lifecycle |
DUPLICATE_REF | 409 | ref already used by this seller | Use a new ref, or fetch the existing escrow |
Authorization
| Code | Status | Cause | Recovery |
|---|---|---|---|
FORBIDDEN | 403 | Authenticated but not allowed | Check role / ownership |
NOT_A_PARTY | 403 | Trying to read/post in an escrow you're not a party to | Buyer or seller only |
NOT_BUYER | 403 | Buyer-only action attempted by seller/admin | Right user, wrong role |
SUSPENDED | 403 | User account is suspended | Contact support |
Rate limiting & body size
| Code | Status | Cause | Recovery |
|---|---|---|---|
RATE_LIMITED | 429 | Too many requests in 1 minute | Honor Retry-After, back off |
PAYLOAD_TOO_LARGE | 413 | Request body > 1 MiB | Trim the body — chat attachments use /api/upload |
Server-side
| Code | Status | Cause | Recovery |
|---|---|---|---|
INTERNAL_ERROR | 500 | Unhandled server error | Retry with same idempotency key |
GATEWAY_TIMEOUT | 504 | Payment gateway didn't respond in time | Retry; 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
- Idempotency — make retries safe
- Reference: Escrows — endpoint-specific errors
- Reference: Webhooks — verifying signatures