Reference
Errors
Every API failure becomes one typed error. This page explains its fields, which codes the SDK generates itself, which come from the API, and how to recover without guessing.
One error class
All API and validation failures throw OBHPaymentsError. Nothing throws a string, and network failures are not raw TypeErrors. The original cause is preserved on error.cause when there is one. The browser guard is the one exception: it throws a plain Error, because the SDK is not meant to run there at all.
import { isOBHPaymentsError } from "@obhco/payments" try { await obh.startPayment(input)} catch (error) { if (isOBHPaymentsError(error)) { console.error(error.code, error.status, error.requestId) } else { // A bug in your code, not an API failure. throw error }}isOBHPaymentsError() is an instanceof check with a type guard. Prefer it over inspecting error.name.
Error fields
| Property | Type | Description |
|---|---|---|
| codeoptional | string | Machine-readable identifier. Either an SDK code or the API error code. |
| statusoptional | number | undefined | HTTP status for API errors. Undefined for local validation, network, and timeout errors. |
| requestIdoptional | string | undefined | Correlation id from the API envelope or the x-request-id response header. |
| detailsoptional | APIErrorDetail[] | undefined | Per-field validation messages, each with an optional field and a message. |
| retryableoptional | boolean | True for timeouts, network errors, 429, and 5xx. False otherwise. |
| messageoptional | string | A human-readable message. API messages are safe to show to operators, not necessarily to customers. |
| causeoptional | unknown | undefined | The underlying error for transport failures. Never shown to customers. |
Details is for fields, not prose
details is populated from validation envelopes. Render it next to the relevant form field when field is present, and as a general message otherwise.
SDK-generated codes
These codes never come from the API. They are produced by the SDK before or around the call.
| Code | When | Retryable | Fix |
|---|---|---|---|
INVALID_ARGUMENT | Constructor or input validation failed — bad amount, URL, timeout, credential prefix, metadata, or idempotency key. | No | Correct the input. No request was sent. |
NETWORK_ERROR | fetch rejected before a response — DNS, TLS, connection reset, or offline. | Yes | Retry with backoff, using the same idempotency key. |
TIMEOUT | The SDK’s timeoutMs elapsed and the request was aborted. | Yes | Retry or retrieve. The outcome is unknown, not failed. |
INVALID_RESPONSE | The response was not JSON, or its data did not match the payment shape. | No | Likely a proxy or version mismatch. Capture the requestId and contact support. |
API_ERROR | An error response had no error.code. Used as a fallback. | Based on status | Inspect status and message. |
API error codes
These arrive in the API’s error envelope and are surfaced verbatim as code. HTTP status is on error.status.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Malformed or invalid request body. Check details. |
| 401 | AUTH_INVALID | Missing, malformed, revoked, or invalid credential. |
| 403 | SERVICE_INACTIVE | The service is deactivated. |
| 404 | PAYMENT_NOT_FOUND | Not found in this service’s scope. |
| 409 | IDEMPOTENCY_CONFLICT | The same key was used for a different request. |
| 409 | ORDER_CONFLICT | The external order already exists with conflicting details. |
| 409 | PAYMENT_STATE_CONFLICT | Invalid lifecycle operation for the current state. |
| 413 | PAYLOAD_TOO_LARGE | The request body exceeds the size limit. |
| 415 | UNSUPPORTED_MEDIA_TYPE | The body is not JSON. |
| 422 | RETURN_URL_NOT_ALLOWED | The return URL is not an exact allowlist match. |
| 422 | RETURN_URL_REQUIRED | No return URL was supplied and no default is configured. |
| 422 | GATEWAY_NOT_SUPPORTED | The gateway code is not one OBH offers. See Gateways & installments. |
| 422 | CUSTOMER_MOBILE_REQUIRED | The chosen gateway (Digipay) needs customerMobile. |
| 503 | GATEWAY_NOT_AVAILABLE | The gateway has no LIVE credentials yet. Offer another one from listGateways(). |
| 429 | RATE_LIMITED | Reserved for per-service rate limiting. Not enforced yet, but treat it as retryable. |
| 502 | PROVIDER_ERROR | Provider transport or response failure, sanitized. |
| 503 | PROVIDER_UNAVAILABLE | No active provider account or adapter. |
Provider errors are sanitized
Raw provider responses, credentials, and request signing details are never passed to connected services. Treat PROVIDER_ERROR as an opaque upstream failure and retry.
The retryable flag
For API errors, retryable is true exactly when the HTTP status is 429 or >= 500. Transport failures (NETWORK_ERROR, TIMEOUT) are retryable. Everything else is not.
| Error | Safe action |
|---|---|
retryable === true on create | Retry with the same idempotency key and backoff. |
retryable === true on retrieve | Retry freely; reads have no side effects. |
retryable === false | Do not retry unchanged. Fix the request, credential, or URL first. |
INVALID_RESPONSE | Do not retry blindly; the response contract is broken. Capture the request id. |
Handling errors well
A good handler answers three questions: can I retry, what do I tell the customer, and what do I record for support?
import { isOBHPaymentsError } from "@obhco/payments" type CheckoutFailure = | { kind: "retry"; message: string } | { kind: "user"; message: string; fields?: string[] } | { kind: "support"; message: string; requestId?: string } export function classify(error: unknown): CheckoutFailure { if (!isOBHPaymentsError(error)) { return { kind: "support", message: "Unexpected error." } } switch (error.code) { case "INVALID_ARGUMENT": case "VALIDATION_ERROR": return { kind: "user", message: "Please check the payment details.", fields: error.details?.map((detail) => detail.field ?? ""), } case "AUTH_INVALID": case "SERVICE_INACTIVE": return { kind: "support", message: "Payments are temporarily unavailable.", requestId: error.requestId, } case "ORDER_CONFLICT": case "IDEMPOTENCY_CONFLICT": return { kind: "user", message: "This order is already being processed.", } case "RETURN_URL_NOT_ALLOWED": case "RETURN_URL_REQUIRED": return { kind: "support", message: "This checkout cannot redirect back to the site.", requestId: error.requestId, } default: return { kind: error.retryable ? "retry" : "support", message: "We could not start the payment.", requestId: error.requestId, } }}Request IDs and support
requestId is the fastest way for support to find a failed request. Show it in an error reference for the customer, and log it next to your own order id and trace id.
It can come from the API error envelope or from the x-request-id response header. The SDK prefers the envelope value when both exist.
Logging hygiene
Never log secrets
Do not log secretKey, webhook signing secrets, Authorization headers, raw provider credentials, or full request bodies that may contain sensitive metadata. Log a request id, an error code, an order id, and a duration instead.
logger.warn("obh.request.failed", { code: error.code, status: error.status, requestId: error.requestId, retryable: error.retryable, orderId: order.id, // Never: secretKey, authorization, raw body})Next: the full API reference, or the troubleshooting table for specific symptoms.