Reference
Configuration
Everything the client accepts, how the base URL is normalized, and the checks that run in the constructor. Configuration errors fail immediately instead of halfway through a checkout.
Client options
new OBHPayments(options: OBHPaymentsOptions) type OBHPaymentsOptions = { secretKey: string simulator?: boolean baseUrl?: string timeoutMs?: number fetch?: typeof fetch}| Property | Type | Description |
|---|---|---|
| secretKeyrequired | string | A server-side service credential beginning with svc_. The constructor rejects anything else. |
| simulatoroptional default: process.env.OBH_SIMULATOR === "true" | boolean | When true, routes payments to the OBH Sadad simulator instead of the live Sadad IPG. |
| baseUrloptional default: "https://obhco.ir/api/v1" | string | Hardcoded to https://obhco.ir/api/v1 by default. Can only be overridden for local OBH core development. |
| timeoutMsoptional default: 15000 | number | Per-request timeout in milliseconds. Must be a finite positive number. |
| fetchoptional default: globalThis.fetch | typeof fetch | Custom fetch implementation, useful for tests, proxies, or instrumented HTTP clients. |
The constructor validates all four options synchronously. A bad credential, an unsafe URL, a non-positive timeout, or a missing fetch implementation throws before any request is made.
Base URL rules
The SDK normalizes baseUrl once, in the constructor. The rules are strict on purpose:
- It must parse as an absolute URL.
- It must not contain credentials, a query string, or a fragment (
user:pass@,?x=1,#hash). - It must use
https:, unless the hostname islocalhost,127.0.0.1, or[::1], where plainhttp:is allowed for development. - Trailing slashes are removed.
- If the resulting path does not end with
/api/v1, that suffix is appended.
| You pass | Requests go to | Why |
|---|---|---|
http://localhost:3100 | http://localhost:3100/api/v1/... | Localhost may use HTTP; suffix appended. |
https://pay.obh.example | https://pay.obh.example/api/v1/... | Standard production form. |
https://pay.obh.example/api/v1 | https://pay.obh.example/api/v1/... | Already suffixed, not doubled. |
https://pay.obh.example/gateway/ | https://pay.obh.example/gateway/api/v1/... | Trailing slash removed, suffix appended to the path. |
http://pay.obh.example | Rejected | Only localhost may use plain HTTP. |
pay.obh.example | Rejected | Not an absolute URL. |
https://pay.obh.example?env=test | Rejected | Query strings are not allowed. |
Prefer an origin, not an endpoint
Pass https://pay.obh.example and let the SDK add the version. Passing the full endpoint works too, but an origin is harder to break when the API version is mentioned elsewhere.
Timeouts
Each request gets its own AbortController and a timer. The default is 15 seconds. When the timer fires, the request is aborted and the SDK throws { code: "TIMEOUT", retryable: true }.
const obh = new OBHPayments({ secretKey: process.env.OBH_SERVICE_KEY!, timeoutMs: 10_000,})A timeout is ambiguous, not a failure: the server may have processed the request. For creation, retry with the same idempotency key. For retrieval, simply retry.
Custom fetch
The package uses the global fetch by default. Supply your own to add tracing, route through a proxy, or simulate responses in tests.
const obh = new OBHPayments({ secretKey: process.env.OBH_SERVICE_KEY!, fetch: async (input, init) => { const startedAt = performance.now() try { return await fetch(input, init) } finally { metrics.observe("obh.request.ms", performance.now() - startedAt) } },})Your fetch must honour AbortSignal
The SDK passes signal on every request. A custom fetch that ignores it will not time out correctly. If you wrap the platform fetch, forward init unchanged apart from what you intentionally add.
Server-only guard
The constructor checks for a global window and throws when one exists. This is a development guard, not a security boundary: it catches the common mistake of importing the SDK into a client component.
// In a browser bundle, this throws:// "@obhco/payments is server-only and must not run in browser code."const obh = new OBHPayments({ /* ... */ })In Next.js, keep the client in a module that is only imported from server code. Adding import "server-only" to that module gives you a second, compile-time guard.
import "server-only" import { OBHPayments } from "@obhco/payments" export const obh = new OBHPayments({ secretKey: process.env.OBH_SERVICE_KEY!,})Environment variables
Use names that make the trust boundary obvious. Never use a NEXT_PUBLIC_ (or equivalent) prefix for secret values.
| Variable | Purpose |
|---|---|
OBH_SERVICE_KEY | The svc_ credential generated for your service. Server-only. |
OBH_SIMULATOR | Optional. When set to "true", routes payments to the OBH Sadad simulator instead of the live Sadad gateway. |
OBH_WEBHOOK_SECRET | Used only by your webhook route, never by the SDK. |
A missing variable is not a nice error
If OBH_SERVICE_KEY is undefined, the constructor throws “secretKey must be an OBH service credential.” Validate configuration at startup so you learn about it before a customer does.
Multiple services
One client represents one credential and one service. Create a separate client for each service you operate; do not share a key across services.
export const igraphical = new OBHPayments({ secretKey: process.env.IGRAPHICAL_SERVICE_KEY!,}) export const otherService = new OBHPayments({ secretKey: process.env.OTHER_SERVICE_KEY!,})A service can only see its own payments. Using the wrong client does not leak data; it produces 404 PAYMENT_NOT_FOUND.
Runtime and module format
- Node.js 20+. The package declares
engines.node >= 20. - ESM only. It exports
importwith TypeScript declarations. CommonJS consumers need a dynamicimport()or an ESM entrypoint. - No runtime dependencies. Node built-ins and web platform globals are enough.
- Edge runtimes. Not officially tested. The client uses standard fetch, AbortController, and crypto, but the server-only guard assumes a
window-less environment. Use a Node.js runtime for now.
Continue with Errors for the failure surface.