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.
Most non-2xx responses use the envelope below. A few do not yet — see Responses outside the envelope. 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 | Selected errors | Structured extras — for example details.required (the missing scopes) on insufficient_permissions, or details.retry_after on 429. |
error.request_id | string | When available | Correlation id for this request. Quote it when contacting support. |
error.doc_url | string | Reserved | Reserved for a link to this error's documentation. Not populated today. |
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 | A concurrent state-transition conflict, or a request with the same Idempotency-Key is still running. |
422 | Unprocessable entity | Business-rule failure (for example, refunding more than the original charge), or an Idempotency-Key reused with a different request. |
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, or a publishable key was used without an Origin (off the browser). |
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. |
503 with code: "NO_PAYMENT_METHODS_AVAILABLE" | No | You have no online payment method for this checkout. Enable one, then retry. |
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_KEY_IN_PROGRESS | Yes | The first request with this key is still running. Back off briefly and retry with the same key. |
422 IDEMPOTENCY_KEY_MISMATCH | No | The key was reused with a different method, path or body. Use a new key only for a genuinely new request. |
Validation errors
Validation errors use type: invalid_request_error. Where RevKeen can identify the offending parameter, error.param names it — for example, a create that references a product that does not exist:
{
"error": {
"type": "invalid_request_error",
"code": "resource_not_found",
"message": "No such product: prod_123",
"param": "product_id"
}
}A request that fails schema validation before it reaches the handler currently returns a different body — see Responses outside the envelope. Treat any 400 as a validation failure, and render the reason next to the offending input rather than surfacing the 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. The per-key API limiter does not send the X-RateLimit-* headers today; rely on Retry-After. |
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
}Error code reference
Every error.code the API can return in the standard envelope, beyond the authentication and authorization codes above.
error.code | Status | Meaning |
|---|---|---|
validation_error | 400 | One or more parameters failed validation. |
invalid_request_error | 400 | The request is malformed or refers to something that cannot be used. |
missing_required_field | 400 | A required parameter was not sent. |
resource_not_found | 404 | The resource does not exist or is not visible to your credential. |
resource_missing | 404 | Same meaning as resource_not_found; some endpoints, including payment intents, return this code. Treat the two alike. |
resource_already_exists | 409 | A resource with the same unique value already exists. |
idempotency_conflict | 409 | An idempotency conflict reported in the standard envelope. The idempotency middleware's own errors use a flat body — see Idempotency errors. |
card_declined | 402 | The issuer declined the card. |
insufficient_funds | 402 | The card has insufficient funds. |
expired_card | 402 | The card has expired. |
invalid_card_number | 402 | The card number is invalid. |
invalid_cvv | 402 | The security code is invalid. |
invalid_expiry | 402 | The expiry date is invalid. |
payment_failed | 402 | The payment could not be completed. |
payment_method_required | 402 | The operation needs a payment method and none is available. |
invoice_already_paid | 400 or 409 | The invoice is already paid. |
invoice_already_voided | 400 or 409 | The invoice is already voided. |
invoice_not_finalized | 400 or 409 | The invoice must be finalized first. |
invoice_already_finalized | 400 or 409 | The invoice is already finalized. |
invoice_has_payments | 400 or 409 | The invoice has payments, so it cannot be changed this way. |
invoice_locked | 400 or 409 | The invoice is locked against changes. |
invoice_pdf_not_ready | 409 | The invoice has no rendered PDF yet, so there is nothing to download. The request does not create one; retry later. |
subscription_already_canceled | 400 or 409 | The subscription is already canceled. |
subscription_already_paused | 400 or 409 | The subscription is already paused. |
subscription_not_paused | 400 or 409 | The subscription is not paused. |
subscription_past_due | 400 or 409 | The subscription is past due. |
order_already_paid | 400 or 409 | The order is already paid. |
order_already_canceled | 400 or 409 | The order is already canceled. |
order_already_fulfilled | 400 or 409 | The order is already fulfilled. |
order_has_payments | 400 or 409 | The order has payments, so it cannot be changed this way. |
rate_limit_exceeded | 429 | See Rate-limit errors. |
internal_error | 500 | An unexpected server error. Retry with backoff and quote error.request_id. |
not_implemented | 501 | The operation is not available. Nothing was changed. Do not retry; use the alternative named in error.message. |
service_unavailable | 503 | A dependency is unavailable. Retry with backoff. |
gateway_error | 5xx | The payment gateway returned an error. Retry with backoff. |
Where two statuses are listed, the one returned depends on the operation. Branch on error.code, not on the status alone.
Responses outside the envelope
These responses do not use the nested error object yet. Handle them by status code and the fields shown:
| Situation | Status | Body |
|---|---|---|
Idempotency-Key errors | 400, 409, 422, 503 | { "error": "<message>", "code": "IDEMPOTENCY_KEY_MISMATCH", "originalRequestId": "..." } — see Idempotency errors. |
| Request-schema validation failure | 400 | { "success": false, "error": { "name": "ZodError", "issues": [ ... ] } } — each issue carries a path and a message. |
| Unknown route | 404 | { "error": "Not Found" } |
Some resource lookups not found (for example GET /v2/customers/{id}) | 404 | { "error": "<message>" } |
| No online payment method for a checkout | 503 | Carries code: "NO_PAYMENT_METHODS_AVAILABLE"; see Retry guidance. |
Examples
The PHP tab uses the published revkeen/sdk-php 1.20260819.1529 package. The Go
tab shows the generated client; the published Go module has a
module-path limitation, so check that page before installing it.
curl -i https://staging-api.revkeen.com/v2/payment-intents/pi_00000000 \
-H "x-api-key: $REVKEEN_API_KEY"
# HTTP/1.1 404 Not Found
# { "error": { "type": "invalid_request_error", "code": "resource_missing", "message": "No such payment intent: 'pi_00000000'" } }See also
Idempotency
Retry mutations safely with an Idempotency-Key.
Pagination
Iterate over list responses correctly.
Versioning
Pin the API version with the RevKeen-Version header.
API Reference
Browse all endpoints and response schemas.
Owner: david · Reviewed 6 Oct 2026 · Next review 6 Nov 2026