Errors
Error envelope, status codes, and retry guidance for the RevKeen API
The RevKeen API returns errors in a consistent envelope with a machine-readable code, a human-readable message, and — where applicable — the offending param and structured field-level details.
Reserve the message for display and the code for branching logic.
Non-2xx status codes always return an error object. Inspect error.code before error.message when writing retry or recovery logic.
Error envelope
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "Customer email is required.",
"param": "customer.email",
"details": {
"fields": {
"customer.email": ["must be a valid email address"]
}
}
}
}| Field | Type | Present on | Purpose |
|---|---|---|---|
error.type | string (enum) | All errors | Broad category — one of invalid_request_error, authentication_error, authorization_error, card_error, rate_limit_error, idempotency_error, api_error. |
error.code | string (enum) | All errors | Stable machine-readable identifier. Safe for logic branching. |
error.message | string | All errors | Human-readable description. Content may change; do not parse. |
error.param | string | Validation errors | Dotted path of the offending parameter. |
error.details | object | Validation errors | Structured extras — most commonly details.fields[param] = [reasons]. |
error.request_id | string | When available | Correlation id for this request. Quote it when contacting support. |
error.doc_url | string | Selected errors | Link to documentation for this error code. |
HTTP status codes
| Status | Meaning | When you see it |
|---|---|---|
400 | Invalid request | Malformed JSON, unknown fields, failed validation. Do not retry without fixing the payload. |
401 | Unauthenticated | Missing, invalid, expired, or revoked credential. Always type: authentication_error, always with a WWW-Authenticate: Bearer challenge header. |
403 | Permission denied | Always type: authorization_error. The key is valid but lacks the required scope (insufficient_permissions), the source IP is outside the key's IP allowlist (ip_not_allowed), or the resource belongs to a different merchant (merchant_mismatch). |
404 | Not found | Resource does not exist, or is not visible to your API key. |
409 | Conflict | Idempotency-Key collision with a different body, or a concurrent state-transition conflict. |
422 | Unprocessable entity | Business-rule failure (for example, refunding more than the original charge). |
429 | Rate limit exceeded | Back off using the Retry-After header. |
500 | Server error | Transient — retry with exponential backoff. |
503 | Service unavailable | Capacity issue — retry with exponential backoff. |
Authentication and authorization errors
401 and 403 are separate categories. A 401 means RevKeen could not establish who is calling. A 403 means the caller is identified and the action is denied. Branch on error.type first, then on error.code.
401 — authentication_error
Every 401 carries an RFC 6750 challenge header:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="revkeen-api"
Content-Type: application/json
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "Authentication required."
}
}error.code | Meaning |
|---|---|
authentication_failed | No usable credential was presented. |
invalid_api_key | The key is malformed or unknown. |
expired_api_key | The key passed its expiry, or its roll grace window has closed. |
api_key_revoked | The key was revoked. Revocation is immediate and permanent — create a new key. |
session_invalid | A session or OAuth bearer token is expired or no longer valid. |
merchant_required | The credential authenticated but resolves to no merchant. |
403 — authorization_error
error.code | Meaning |
|---|---|
insufficient_permissions | The key is valid but lacks a scope the endpoint requires. |
forbidden | The operation is not permitted for this caller. |
ip_not_allowed | The source IP is outside the key's IP allowlist. |
merchant_mismatch | The resource belongs to a different merchant. |
tenant_scope_mismatch | The requested tenant disagrees with the credential's tenant. |
no_organization, no_merchant | The caller has no organization or merchant context. |
origin_not_allowed | The browser Origin is not registered for this merchant's storefront. |
cart_disabled | Cart sessions are not enabled for this merchant. |
verification_required | The account is not yet verified for this operation. |
publishable_key_restricted | A publishable key attempted a secret-key-only operation. |
read_only_impersonation_blocked | A read-only support session attempted a write. |
internal_only | The endpoint is not part of the public API. |
Error bodies never echo the credential you presented, in message or in details.
Retry guidance
| Status / Condition | Retryable? | Strategy |
|---|---|---|
500, 502, 503, 504 | Yes | Exponential backoff, at least 3 attempts, capped at ~30s total. |
429 | Yes | Honour the Retry-After header (seconds). Do not retry sooner. |
| Network timeout on a mutation | Yes | Retry with the same Idempotency-Key — see Idempotency. |
400, 404, 422 | No | Permanent — fix the payload or resource first. |
401, 403 | No | Rotate or rescope the API key, then retry. |
409 (idempotency conflict) | No | The key was reused with a different body. Generate a new key. |
Validation errors
Validation errors (type: invalid_request_error, code: validation_error) always include a details.fields map. Keys are dotted parameter paths; values are arrays of reasons.
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "One or more fields are invalid.",
"details": {
"fields": {
"items[0].quantity": ["must be greater than 0"],
"customer.email": ["must be a valid email address"]
}
}
}
}Render field errors next to the offending input rather than surfacing message alone.
Rate-limit errors
429 Too Many Requests always carries type: rate_limit_error, code: rate_limit_exceeded, a Retry-After header in whole seconds, and details.retry_after with the same value.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after the period in Retry-After.",
"details": { "retry_after": 12 }
}
}| Header | Always present | Meaning |
|---|---|---|
Retry-After | Yes | Whole seconds to wait before retrying. Honour this instead of computing your own backoff. |
X-RateLimit-Limit | No | Requests permitted in the current window, when the limiter publishes one. |
X-RateLimit-Remaining | No | Requests left in the current window. |
X-RateLimit-Reset | No | UNIX timestamp in seconds — not milliseconds — at which the window resets. |
Retry-After, WWW-Authenticate and the X-RateLimit-* trio are all exposed through CORS on /v2, so a browser fetch client can read them.
Published plan limits are on the rate limits reference.
Reading the headers from an SDK
Every RevKeen SDK surfaces all four headers on the error it throws, so you do not
have to reach for the raw response. A header the server did not send reads as
null rather than zero, so a published X-RateLimit-Remaining: 0 is never
confused with an unreported limit. The reset instant is parsed from UNIX
seconds, and the retry delay is reported exactly as the server stated it — the
SDK's own retry backoff is capped, this value is not.
import { RevKeenRateLimitError } from "@revkeen/sdk";
try {
await revkeen.invoices.list();
} catch (error) {
if (error instanceof RevKeenRateLimitError) {
error.rateLimit.retryAfterSeconds; // number | null — Retry-After
error.rateLimit.limit; // number | null — X-RateLimit-Limit
error.rateLimit.remaining; // number | null — X-RateLimit-Remaining
error.rateLimit.resetAt; // Date | null — X-RateLimit-Reset
}
}var requestError *revkeen.RequestError
if errors.As(err, &requestError) && requestError.Kind == revkeen.ErrorKindRateLimit {
requestError.RateLimit.RetryAfter // *time.Duration
requestError.RateLimit.Limit // *int64
requestError.RateLimit.Remaining // *int64
requestError.RateLimit.ResetAt // *time.Time
}try {
$api->invoicesList();
} catch (\RevKeen\Runtime\RateLimitException $error) {
$error->getRateLimit()->getRetryAfterSeconds(); // ?int
$error->getRateLimit()->getLimit(); // ?int
$error->getRateLimit()->getRemaining(); // ?int
$error->getRateLimit()->getResetAt(); // ?DateTimeImmutable
}Examples
The Go and PHP tabs show generated source shapes only. Their supported package channels are not live; use cURL or the available TypeScript SDK today.
curl -i https://staging-api.revkeen.com/v2/customers/cus_does_not_exist \
-H "x-api-key: $REVKEEN_API_KEY"
# HTTP/1.1 404 Not Found
# { "error": { "type": "invalid_request_error", "code": "resource_missing", "message": "Customer not found." } }