OBHPayments
↑↓ navigate↵ openSearches titles and topics
Documentation

Operations

Troubleshooting

Start from the error code or the exact message. Most failures are configuration errors that the constructor catches, or conflicts that require reconciliation rather than another retry.

Quick diagnosis

What you seeMost likely causeWhere to look
Throws immediately, before any requestConstructor validation (INVALID_ARGUMENT) or the browser guardServer-only errors
401 AUTH_INVALIDWrong, malformed, or revoked credentialAuthentication
409 ORDER_CONFLICTSame order id with different detailsConflicts
422 RETURN_URL_...Return URL missing or not exactly allowlistedReturn URLs
TIMEOUT or NETWORK_ERRORUnknown outcome; do not assume failureTimeouts
Status stays pendingProvider has not settled, or the payment was abandonedStuck pending
INVALID_RESPONSEProxy, wrong base URL, or API version mismatchInstall and build

Common errors

Message or codeFix
baseUrl must be an absolute URL.Pass a full origin such as https://pay.obh.example, not a hostname or a path. Check for an undefined environment variable.
baseUrl must use HTTPS except for localhost development.Use https://. Plain HTTP is allowed only for localhost, 127.0.0.1, and [::1].
baseUrl cannot contain credentials, query, or hash.Remove user:pass@, ?params, and #fragment.
secretKey must be an OBH service credential.The key must begin with svc_. Check that the environment variable is loaded in the server process.
amount must be a positive decimal string.Use a string like "245000000" or "1000.50". No numbers, no zero, no negative values, at most two decimals.
Only IRR is supported.Omit currency or set it to "IRR".
metadata cannot exceed 8 KB.Store large documents elsewhere and keep only ids or small flags in metadata.
The OBH API could not be reached.Check DNS, TLS, firewall, and the base URL. Retry with backoff using the same idempotency key.
The OBH API returned an unexpected payment shape.The response did not match the payment contract. Confirm the SDK and API versions, then report the requestId.

Server-only errors

The message is explicit: @obhco/payments is server-only and must not run in browser code.

  • A file with "use client" imports the module — directly or through a shared helper.
  • A page component is being bundled for the client.
  • A test environment defines a global window.

Move the call into a route handler, server action, or server module.

lib/obh.tsts
import "server-only" // fails the build if imported by a client component import { OBHPayments } from "@obhco/payments" export const obh = new OBHPayments({  secretKey: process.env.OBH_SERVICE_KEY!,  baseUrl: process.env.OBH_API_URL!,})

Do not try to “fix” this with a polyfill or by setting window = undefined. Moving the secret into the browser is the actual problem.

Authentication

CodeChecks
AUTH_INVALID (401)Is the header exactly Bearer svc_...? Has the credential been revoked? Is there a stray whitespace or newline in the environment variable?
SERVICE_INACTIVE (403)The service is deactivated. Contact an OBH administrator.
PAYMENT_NOT_FOUND (404)Wrong reference, wrong service, or wrong environment. Another service’s payment deliberately looks identical to a missing one.

A trailing newline is a real bug

Secrets pasted from a terminal or a dashboard often carry a trailing newline. The credential then fails authentication with a valid-looking key. Trim environment values, or let your config loader do it.

Conflicts

ORDER_CONFLICT

The external order id already exists but the new request has different financial details. Retrieve the existing payment by reference and reconcile. If the customer genuinely needs a new payment, use a new order id (or a new attempt that the API treats as a new order).

IDEMPOTENCY_CONFLICT

A key was reused for a different request. Do not generate a new key and fire the request again — that can create a second payment. Inspect the key derivation and the request fingerprint.

ts
// Wrong: the second attempt silently creates a new key.obh.startPayment({ ...input, idempotencyKey: crypto.randomUUID() }) // Right: one key per logical attempt, reused across retries.obh.startPayment({ ...input, idempotencyKey: `order-${id}-attempt-${n}` })

Timeouts and unknown outcomes

TIMEOUT and NETWORK_ERROR mean the SDK did not receive a definitive answer. They do not mean the payment failed.

  1. If you already stored a reference, call getPayment().
  2. If you have no reference, retry startPayment() with the same idempotency key.
  3. Do not tell the customer the payment failed based on a timeout.
  4. Do not switch to a new key “to be safe”.

Payments stuck pending

A pending payment is normal for seconds, sometimes longer. It resolves through server-side verification; there is no admin endpoint in this SDK to force it.

  • Confirm the customer actually completed checkout.
  • Check the payment’s verifiedAt and status.
  • Stop polling after a bounded number of attempts.
  • For a test environment, ensure the development adapter is enabled and that you completed the local checkout simulation.
  • If it stays pending long after checkout, record the reference and request id and escalate. Do not fabricate a completion.

Install and build issues

SymptomCause and fix
Cannot find module '@obhco/payments'The workspace dependency is missing. Add "@obhco/payments": "workspace:*" and reinstall. The package is not on npm.
“Unexpected token export” in a CommonJS runtimeThe package is ESM-only. Use import or dynamic import().
Types resolve to dist but the folder is emptyRun pnpm --filter @obhco/payments build first.
Edge runtime runtime errorsUse the Node.js runtime. Edge is not officially tested for this SDK.
INVALID_RESPONSE behind a proxyA proxy may be returning an HTML error page. Check the status and body, and allowlist the OBH origin.

FAQ

Is the SDK production-ready?

No. It is published on npm as `@obhco/payments@0.1.0` and tested locally only. It has never processed a real payment.

Can I use it in a browser or React client component?

No. There is no browser entrypoint; the constructor throws. There is also no reason to — the secret is server-side by design.

Can I get a LIVE credential?

Not yet. Only TEST credentials are issued until a live provider adapter is approved and tested.

Does the SDK support refunds, cancellation, or captures?

No. The API exposes only create and retrieve. Do not design a refund flow around this version.

How do I charge for multiple items?

Send one amount that is the order total, put a human-readable summary in description, and keep the line items in your own database (optionally an order id in metadata).

Does the SDK retry automatically?

No. It makes exactly one request per call. Retries are your decision, so that you control idempotency, backoff, and user experience.

Can I call the HTTP API without the SDK?

Yes. The SDK adds validation, typing, timeouts, and error mapping. If you implement those yourself, the HTTP reference is complete.

Why is checkoutUrl null?

Some adapters complete the payment without a redirect. Check the status and design your UI to handle a missing URL instead of asserting it.

Which Node.js version do I need?

Node.js 20 or newer, as declared by the package’s engines field.