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 see | Most likely cause | Where to look |
|---|---|---|
| Throws immediately, before any request | Constructor validation (INVALID_ARGUMENT) or the browser guard | Server-only errors |
401 AUTH_INVALID | Wrong, malformed, or revoked credential | Authentication |
409 ORDER_CONFLICT | Same order id with different details | Conflicts |
422 RETURN_URL_... | Return URL missing or not exactly allowlisted | Return URLs |
TIMEOUT or NETWORK_ERROR | Unknown outcome; do not assume failure | Timeouts |
Status stays pending | Provider has not settled, or the payment was abandoned | Stuck pending |
INVALID_RESPONSE | Proxy, wrong base URL, or API version mismatch | Install and build |
Common errors
| Message or code | Fix |
|---|---|
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.
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
| Code | Checks |
|---|---|
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.
// 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.
- If you already stored a reference, call
getPayment(). - If you have no reference, retry
startPayment()with the same idempotency key. - Do not tell the customer the payment failed based on a timeout.
- 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
verifiedAtandstatus. - 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
| Symptom | Cause 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 runtime | The package is ESM-only. Use import or dynamic import(). |
Types resolve to dist but the folder is empty | Run pnpm --filter @obhco/payments build first. |
| Edge runtime runtime errors | Use the Node.js runtime. Edge is not officially tested for this SDK. |
INVALID_RESPONSE behind a proxy | A 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.