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"]
      }
    }
  }
}
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.detailsobjectValidation errorsStructured extras — most commonly details.fields[param] = [reasons].
error.request_idstringWhen availableCorrelation id for this request. Quote it when contacting support.
error.doc_urlstringSelected errorsLink to documentation for this error code.

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.
409ConflictIdempotency-Key collision with a different body, or a concurrent state-transition conflict.
422Unprocessable entityBusiness-rule failure (for example, refunding more than the original charge).
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.
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.
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 conflict)NoThe 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 }
  }
}
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.
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
}

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." } }

See also

On this page