Errors

Understand Tokeflow's standard error envelope, error types, HTTP status codes, and how to handle failures reliably in production.

Tokeflow returns errors in a single, predictable shape across every endpoint. Build your error handling once, and it works everywhere.

Every failed request returns:

  • A meaningful HTTP status code (the transport-level signal).
  • A structured error envelope with a machine-readable type and code, a human-readable message, and a request_id for support.

Lead with the type and code in your code paths, surface the message to humans, and always log the request_id.

A successful response is wrapped in the success envelope (success, data, meta). A failed response is not — it has a single top-level error object and no success field. Branch on the HTTP status code first.

The error envelope

{
  "error": {
    "type": "validation_error",
    "code": "INVALID_FIELD",
    "message": "Validation failed",
    "details": {
      "amount": ["amount must be a positive integer"]
    },
    "request_id": "req_8f3c2a1b9d7e4f60",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}
FieldTypeAlways presentDescription
typestringYesHigh-level error category. One of the error types below. Use this to branch your handling.
codestringYesStable, machine-readable code (e.g. INVALID_FIELD, RESOURCE_NOT_FOUND). More specific than type. Safe to switch on.
messagestringYesHuman-readable explanation. Display-safe, but may change wording over time — never parse it.
detailsobjectNoOptional context. For validation errors, a map of field name to an array of messages. May be omitted entirely.
request_idstringYesUnique identifier for this request (req_…). Log it and include it in any support request.
timestampstringYesISO 8601 UTC time the error was generated.

Switch on type for broad handling (retry vs. fix-the-request) and on code when you need to react to a specific failure (for example, IDEMPOTENCY_KEY_CONFLICT vs. RESOURCE_ALREADY_EXISTS, both under conflict_error). Treat code as the stable contract; treat message as display-only.

How errors flow

Error types

Each error carries a type that maps one-to-one to an HTTP status code. Use the status code to decide your strategy; use type and code to decide your behavior.

TypeHTTPMeaningHow to handle
validation_error400The request body or query parameters failed validation.Fix the request. Inspect details for field-level messages. Do not retry unchanged.
bad_request_error400The request was malformed or semantically invalid in a way that is not field validation.Fix the request. Do not retry unchanged.
authentication_error401The API key is missing, malformed, or invalid.Check the Authorization header and key. Do not retry unchanged.
authorization_error403The key is valid but lacks the required scope, or the target resource is out of the key's scope.Use a key with the right scope. Do not retry unchanged.
not_found_error404The resource or endpoint does not exist (or is outside your scope).Check the ID and path. Do not retry unchanged.
conflict_error409The request conflicts with the current state (e.g. duplicate, idempotency-key reuse, resource in use).Resolve the conflict, then retry. See code for specifics.
business_rule_error422The request was well-formed but violates a business rule (e.g. an invalid state transition).Fix the operation to satisfy the rule. Do not retry unchanged.
rate_limit_error429You exceeded the rate limit for this API key.Back off and retry with exponential backoff.
external_service_error502A downstream provider in the orchestration path returned an error or was unreachable.Retry with backoff. If it persists, inspect the resource's status.
internal_server_error500An unexpected error occurred on Tokeflow's side.Retry with backoff. If it persists, contact support with the request_id.

The code field is more granular than type. For example, a conflict_error may carry RESOURCE_ALREADY_EXISTS, DUPLICATE_ENTRY, RESOURCE_IN_USE, or IDEMPOTENCY_KEY_CONFLICT. Branch on type for strategy, on code for specifics.

Retry strategy at a glance

  • 4xx (except 429): fix the request. The problem is in the input — a bad field, a missing scope, an unknown ID, or a rule violation. Retrying the same request will fail the same way.
  • 429: back off. You are being rate limited. Retry with exponential backoff and jitter.
  • 5xx (500, 502): retryable. These are transient. Retry idempotently with exponential backoff. Always send an Idempotency-Key on create/charge mutations so retries never duplicate work.

Never retry a non-429 4xx unchanged — it will fail identically and waste your rate-limit budget. Read type, code, and details, fix the request, then try again.

Example error responses

400 — Validation error (validation_error)

Field-level failures appear under details, keyed by field name. Each value is an array of messages.

{
  "error": {
    "type": "validation_error",
    "code": "INVALID_FIELD",
    "message": "Validation failed",
    "details": {
      "amount": ["amount must be a positive integer"],
      "currency": ["currency must be a valid ISO 4217 code"]
    },
    "request_id": "req_8f3c2a1b9d7e4f60",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

Amounts are integer minor units (e.g. 15000 for R$150.00) and currency is ISO 4217. See Conventions for money and formatting rules.

401 — Authentication error (authentication_error)

The API key is missing, malformed, or invalid.

{
  "error": {
    "type": "authentication_error",
    "code": "INVALID_CREDENTIALS",
    "message": "Authentication failed",
    "request_id": "req_3a91c0ed7b224f18",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

Secret keys (sk_…) are server-side only and must never appear in browser code. Send them in the Authorization: Bearer <secret_key> header from your backend. See Authentication.

403 — Authorization error (authorization_error)

The key is valid but lacks the required scope, or the target resource is outside the key's scope.

{
  "error": {
    "type": "authorization_error",
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "This API key does not have the required scope: transactions:write",
    "request_id": "req_77ba2c41e9f04d3a",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

Scopes follow resource:action (e.g. transactions:read, products:write). When using an Organization key against merchant-scoped resources, you must also identify the target merchant via the merchant_id query parameter or body field. See Authentication.

404 — Not found error (not_found_error)

The resource does not exist, or it is outside the scope of your key.

{
  "error": {
    "type": "not_found_error",
    "code": "RESOURCE_NOT_FOUND",
    "message": "Transaction not found",
    "request_id": "req_5e0d8f3a6c124b97",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

409 — Conflict error (conflict_error)

The request conflicts with the current state. A common cause is replaying an Idempotency-Key whose body differs from the original request.

{
  "error": {
    "type": "conflict_error",
    "code": "IDEMPOTENCY_KEY_CONFLICT",
    "message": "This idempotency key was already used with a different request body.",
    "request_id": "req_1c47a98f2b6e40d5",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

Replaying the same Idempotency-Key with the same body returns the original result (HTTP 200 on replay vs. 201 on first create) — not a conflict. A conflict only occurs when the key is reused with a different payload. See Idempotency.

Other conflict codes include RESOURCE_ALREADY_EXISTS (a record with these values already exists), DUPLICATE_ENTRY, and RESOURCE_IN_USE (the resource is referenced by other records and cannot be deleted or modified).

422 — Business rule error (business_rule_error)

The request was well-formed but violates a business rule — for example, attempting an invalid state transition.

{
  "error": {
    "type": "business_rule_error",
    "code": "INVALID_STATE_TRANSITION",
    "message": "Cannot capture a transaction that has already been voided.",
    "details": {
      "current_status": "voided",
      "requested_action": "capture"
    },
    "request_id": "req_9b2f6d1a3e8c47f0",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

422 means "I understood the request, but it cannot be applied in the current state." Inspect the resource's current status before retrying. Related codes include TRANSACTION_ALREADY_VOIDED, TRANSACTION_ALREADY_CAPTURED, and TRANSACTION_ALREADY_REFUNDED.

429 — Rate limit error (rate_limit_error)

You exceeded the request limit for this API key. The default limit is 100 requests per 60-second window, tracked per key.

{
  "error": {
    "type": "rate_limit_error",
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Retry after a short delay.",
    "request_id": "req_4d8e1a07f5c9426b",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

On 429, retry with exponential backoff and jitter rather than a fixed delay. Spreading retries prevents a thundering herd when many clients back off at once.

5xx — Server and upstream errors (internal_server_error, external_service_error)

A 500 indicates an unexpected error on Tokeflow's side. A 502 indicates a downstream provider in the orchestration path failed or was unreachable. Both are transient — retry with backoff.

{
  "error": {
    "type": "internal_server_error",
    "code": "INTERNAL_ERROR",
    "message": "Internal server error",
    "request_id": "req_a1b2c3d4e5f60718",
    "timestamp": "2026-01-15T12:30:00.000Z"
  }
}

If a 5xx persists after several backed-off retries, contact support and include the request_id. It lets us trace the exact request end to end.

Handling errors in code

Read the HTTP status to decide whether to retry, and the envelope to decide how to react. The example below retries 429 and 5xx with exponential backoff and surfaces everything else for the caller to fix.

async function tokeflowRequest(url, options, attempt = 0) {
  const MAX_RETRIES = 4;
  const res = await fetch(url, options);

  if (res.ok) {
    return res.json();
  }

  const body = await res.json();
  const { type, code, message, request_id } = body.error;

  // Always log the request_id — it is your support handle.
  console.error(`Tokeflow error ${res.status} ${code}: ${message} (request_id=${request_id})`);

  const isRetryable = res.status === 429 || res.status >= 500;

  if (isRetryable && attempt < MAX_RETRIES) {
    // Exponential backoff with jitter: 0.5s, 1s, 2s, 4s (+/- jitter).
    const delay = Math.pow(2, attempt) * 500 + Math.random() * 250;
    await new Promise((r) => setTimeout(r, delay));
    return tokeflowRequest(url, options, attempt + 1);
  }

  // 4xx (except 429): the request must change. Surface it to the caller.
  const error = new Error(message);
  Object.assign(error, { type, code, request_id, status: res.status });
  throw error;
}
# Inspect the status code and envelope on a failed request.
curl -i https://api.tokeflow.com/api/v1/transactions/tx_does_not_exist \
  -H "Authorization: Bearer sk_live_mer_xxxxxxxxxxxxxxxx"

# HTTP/1.1 404 Not Found
# {
#   "error": {
#     "type": "not_found_error",
#     "code": "RESOURCE_NOT_FOUND",
#     "message": "Transaction not found",
#     "request_id": "req_5e0d8f3a6c124b97",
#     "timestamp": "2026-01-15T12:30:00.000Z"
#   }
# }

Best practices

  • Log request_id on every failure. It is the fastest way for support to trace a request. Include it in tickets and internal logs.
  • Branch on type and code, never on message. Messages are for humans and may change; type and code are the stable contract.
  • Treat 5xx and 429 as retryable with exponential backoff and jitter; treat other 4xx as fix-the-request.
  • Always send an Idempotency-Key on create/charge mutations so safe retries never duplicate work.
  • Read details on 400 to pinpoint the exact field that failed validation.
  • Conventions — envelopes, pagination, idempotency, money, and rate limits.
  • Authentication — keys, scopes, and headers.
  • Webhooks — signature verification and event delivery.

On this page