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"]
      }
    }
  }
}
FieldTypePresent onPurpose
error.typestring (enum)All errorsBroad category — one of invalid_request_error, authentication_error, authorization_error, card_error, rate_limit_error, idempotency_error, api_error.
error.codestring (enum)All errorsStable machine-readable identifier. Safe for logic branching.
error.messagestringAll errorsHuman-readable description. Content may change; do not parse.
error.paramstringValidation errorsDotted path of the offending parameter.
error.detailsobjectSelected errorsStructured extras — for example details.required (the missing scopes) on insufficient_permissions, or details.retry_after on 429.
error.request_idstringWhen availableCorrelation id for this request. Quote it when contacting support.
error.doc_urlstringReservedReserved for a link to this error's documentation. Not populated today.

HTTP status codes

StatusMeaningWhen you see it
400Invalid requestMalformed JSON, unknown fields, failed validation. Do not retry without fixing the payload.
401UnauthenticatedMissing, invalid, expired, or revoked credential. Always type: authentication_error, always with a WWW-Authenticate: Bearer challenge header.
403Permission deniedAlways 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).
404Not foundResource does not exist, or is not visible to your API key.
409ConflictA concurrent state-transition conflict, or a request with the same Idempotency-Key is still running.
422Unprocessable entityBusiness-rule failure (for example, refunding more than the original charge), or an Idempotency-Key reused with a different request.
429Rate limit exceededBack off using the Retry-After header.
500Server errorTransient — retry with exponential backoff.
503Service unavailableCapacity 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.codeMeaning
authentication_failedNo usable credential was presented.
invalid_api_keyThe key is malformed or unknown.
expired_api_keyThe key passed its expiry, or its roll grace window has closed.
api_key_revokedThe key was revoked. Revocation is immediate and permanent — create a new key.
session_invalidA session or OAuth bearer token is expired or no longer valid.
merchant_requiredThe credential authenticated but resolves to no merchant.

403 — authorization_error

error.codeMeaning
insufficient_permissionsThe key is valid but lacks a scope the endpoint requires.
forbiddenThe operation is not permitted for this caller.
ip_not_allowedThe source IP is outside the key's IP allowlist.
merchant_mismatchThe resource belongs to a different merchant.
tenant_scope_mismatchThe requested tenant disagrees with the credential's tenant.
no_organization, no_merchantThe caller has no organization or merchant context.
origin_not_allowedThe browser Origin is not registered for this merchant's storefront, or a publishable key was used without an Origin (off the browser).
cart_disabledCart sessions are not enabled for this merchant.
verification_requiredThe account is not yet verified for this operation.
publishable_key_restrictedA publishable key attempted a secret-key-only operation.
read_only_impersonation_blockedA read-only support session attempted a write.
internal_onlyThe endpoint is not part of the public API.

Error bodies never echo the credential you presented, in message or in details.

Retry guidance

Status / ConditionRetryable?Strategy
500, 502, 503, 504YesExponential backoff, at least 3 attempts, capped at ~30s total.
503 with code: "NO_PAYMENT_METHODS_AVAILABLE"NoYou have no online payment method for this checkout. Enable one, then retry.
429YesHonour the Retry-After header (seconds). Do not retry sooner.
Network timeout on a mutationYesRetry with the same Idempotency-Key — see Idempotency.
400, 404, 422NoPermanent — fix the payload or resource first.
401, 403NoRotate or rescope the API key, then retry.
409 IDEMPOTENCY_KEY_IN_PROGRESSYesThe first request with this key is still running. Back off briefly and retry with the same key.
422 IDEMPOTENCY_KEY_MISMATCHNoThe 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 }
  }
}
HeaderAlways presentMeaning
Retry-AfterYesWhole seconds to wait before retrying. Honour this instead of computing your own backoff.
X-RateLimit-LimitNoRequests 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-RemainingNoRequests left in the current window.
X-RateLimit-ResetNoUNIX 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.codeStatusMeaning
validation_error400One or more parameters failed validation.
invalid_request_error400The request is malformed or refers to something that cannot be used.
missing_required_field400A required parameter was not sent.
resource_not_found404The resource does not exist or is not visible to your credential.
resource_missing404Same meaning as resource_not_found; some endpoints, including payment intents, return this code. Treat the two alike.
resource_already_exists409A resource with the same unique value already exists.
idempotency_conflict409An idempotency conflict reported in the standard envelope. The idempotency middleware's own errors use a flat body — see Idempotency errors.
card_declined402The issuer declined the card.
insufficient_funds402The card has insufficient funds.
expired_card402The card has expired.
invalid_card_number402The card number is invalid.
invalid_cvv402The security code is invalid.
invalid_expiry402The expiry date is invalid.
payment_failed402The payment could not be completed.
payment_method_required402The operation needs a payment method and none is available.
invoice_already_paid400 or 409The invoice is already paid.
invoice_already_voided400 or 409The invoice is already voided.
invoice_not_finalized400 or 409The invoice must be finalized first.
invoice_already_finalized400 or 409The invoice is already finalized.
invoice_has_payments400 or 409The invoice has payments, so it cannot be changed this way.
invoice_locked400 or 409The invoice is locked against changes.
invoice_pdf_not_ready409The invoice has no rendered PDF yet, so there is nothing to download. The request does not create one; retry later.
subscription_already_canceled400 or 409The subscription is already canceled.
subscription_already_paused400 or 409The subscription is already paused.
subscription_not_paused400 or 409The subscription is not paused.
subscription_past_due400 or 409The subscription is past due.
order_already_paid400 or 409The order is already paid.
order_already_canceled400 or 409The order is already canceled.
order_already_fulfilled400 or 409The order is already fulfilled.
order_has_payments400 or 409The order has payments, so it cannot be changed this way.
rate_limit_exceeded429See Rate-limit errors.
internal_error500An unexpected server error. Retry with backoff and quote error.request_id.
not_implemented501The operation is not available. Nothing was changed. Do not retry; use the alternative named in error.message.
service_unavailable503A dependency is unavailable. Retry with backoff.
gateway_error5xxThe 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:

SituationStatusBody
Idempotency-Key errors400, 409, 422, 503{ "error": "<message>", "code": "IDEMPOTENCY_KEY_MISMATCH", "originalRequestId": "..." } — see Idempotency errors.
Request-schema validation failure400{ "success": false, "error": { "name": "ZodError", "issues": [ ... ] } } — each issue carries a path and a message.
Unknown route404{ "error": "Not Found" }
Some resource lookups not found (for example GET /v2/customers/{id})404{ "error": "<message>" }
No online payment method for a checkout503Carries 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

Owner: david · Reviewed 6 Oct 2026 · Next review 6 Nov 2026

On this page