Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

Errors

Errors are part of the API contract. Every error response — 4xx and 5xx — has the same shape, and the SDKs surface them as typed exception classes.

Response shape

HTTP/1.1 400 Bad Request
{
"error": {
"type": "invalid_parameter",
"code": "missing_field",
"message": "Field `holder.email` is required.",
"param": "holder.email",
"request_id": "req_01HZX",
"docs_url": "https://developers.qairopay.com/concepts/errors#missing_field"
}
}
  • type — broad classification (see below).
  • code — specific machine-readable identifier within the type.
  • message — human-readable explanation, safe to surface to end users (it never includes raw input).
  • param — for invalid_parameter errors, the JSON pointer to the offending field.
  • request_id — include this when you contact support.
  • docs_url — links to the documentation for that specific code.

Error types

TypeHTTPMeaningRecovery
invalid_parameter400The request body failed validation.Fix the body. Look at param.
unauthenticated401No key, malformed key, or revoked key.Check the Authorization header.
insufficient_scope403Key is valid but lacks the required scope.Use a key with the correct role.
not_found404The resource doesn’t exist, or doesn’t exist in this tenant.Confirm the id; cross-tenant access returns 404, not 403.
idempotency_conflict409Same Idempotency-Key reused with a different body.Generate a new key.
idempotency_inflight409A retry arrived while the original is still running.Wait retry_after_ms and retry.
conflict409Resource state precludes the operation (e.g. revoking an already-revoked pass).Fetch the current state and decide whether to proceed.
rate_limited429You exceeded the rate limit for this endpoint.Honor Retry-After. See Rate limits.
validation_failed422Business-rule validation failed (e.g. KYB rejected, sanctions hit).The code will tell you what; some are user-correctable, some require manual review.
card_declined402Card-network decline. The processor decline reason is in code.Surface to the user; some declines are retryable, some are not.
provider_error502Upstream provider (Stripe, Persona, Bridge, card network) failed.Retry with exponential backoff.
internal_error500We dropped the ball.Retry with exponential backoff. The platform’s SLA covers these.
service_unavailable503Maintenance or partial outage.Honor Retry-After. Check status.

What’s retryable

  • Always retry: provider_error (502), internal_error (500), service_unavailable (503).
  • Retry with backoff: rate_limited (429), idempotency_inflight (409).
  • Never retry without code changes: invalid_parameter, unauthenticated, insufficient_scope, idempotency_conflict, validation_failed.
  • Depends on the decline code: card_declined. The reference for each decline code says whether it’s terminal or retryable.

The TypeScript SDK retries idempotent retryable errors automatically (default: 3 attempts with exponential backoff). Disable per-call with { retries: 0 } if you need fine-grained control.

Catching errors in the SDK

The TypeScript SDK ships a typed exception hierarchy — one subclass per API error.type, plus three local-only subclasses for SDK-side conditions:

import {
QairoPayError, // base — every subclass extends it
InvalidParameterError, // 400 invalid_parameter
UnauthenticatedError, // 401 unauthenticated
InsufficientScopeError, // 403 insufficient_scope
NotFoundError, // 404 not_found
IdempotencyConflictError, // 409 idempotency_conflict
IdempotencyInflightError, // 409 idempotency_inflight
ConflictError, // 409 conflict
ValidationFailedError, // 422 validation_failed
RateLimitError, // 429 rate_limited
CardDeclinedError, // 402 card_declined (.declineReason accessor)
ProviderError, // 502 provider_error
InternalError, // 500 internal_error
ServiceUnavailableError, // 503 service_unavailable
// Local-only — not produced by the API:
TimeoutError, // outer timeout fired (.timeoutMs)
MalformedResponseError, // 2xx with unparseable body (.rawBody)
WebhookSignatureError, // verification failed (.reason)
} from "@qairopay/sdk";
try {
await qp.passes.create({ template_id: "tpl_x", holder: { email: "[email protected]" } });
} catch (err) {
if (err instanceof InvalidParameterError) {
// err.param identifies which field failed; err.code is machine-readable.
return showFieldError(err.param!, err.message);
}
if (err instanceof CardDeclinedError) {
// declineReason equals .code (e.g., "insufficient_funds").
return showDeclineUI(err.declineReason);
}
if (err instanceof RateLimitError) {
// err.retryAfterMs from the platform; SDK already waited and retried up to budget.
return scheduleRetry(err.retryAfterMs);
}
if (err instanceof QairoPayError) {
// Catch-all for any platform error, including forward-compat for
// new error.type values added in future API releases.
console.error(err.requestId, err.type, err.code, err.message);
}
throw err;
}

Forward-compat (FR-018): if a future API release adds a new error.type the SDK doesn’t know about yet, it falls back to the base QairoPayError with every envelope field populated. Your existing instanceof QairoPayError blocks keep working — no upgrade required to handle unknown types safely.

Surfacing errors to end users

message is always safe to surface verbatim — it never includes secrets or unparsed user input. That said, you’ll usually want to translate it to your product’s tone. The TS SDK exposes a userMessage helper that returns a localized, customer-friendly version of the standard library of codes.

When in doubt

Include the request_id in any support ticket. Without it we can find your request, but it takes longer.