Getting started
Quickstart
From an empty project to a verified test payment. This takes about ten minutes and needs one server-side credential — no provider account, no dashboard setup.
Before you begin
| Requirement | Why |
|---|---|
| Node.js 20 or newer | The package uses modern fetch and Web Crypto APIs. |
| A service credential | A key shaped like svc_<id>.<secret>, generated automatically when your service is created in OBH Admin. |
| A return URL | An absolute URL registered in the service’s exact allowlist or set as its default return URL. |
| A server runtime | A Node.js process, Next.js route handler, server action, worker, or similar. Never browser code. |
No credential yet?
A service and its TEST credential are created by an OBH administrator in OBH Admin. The SDK cannot create credentials, and there is no self-service signup.
1. Install the package
Inside this monorepo the package is available through the workspace. From a standalone project, install it with your package manager.
# In this monorepo, add the workspace dependencypnpm --filter web add @obhco/payments@workspace:* # The package is not on npm yet, so a public install is not possible# pnpm add @obhco/paymentsThe package is ESM-only and declares "type": "module". A CommonJS project can still use it via dynamic import(), but native ESM is the supported path.
2. Add your credentials
Keep the service key in server-side environment configuration. The secret must never reach a browser bundle, a client component, or a log.
# Server-only. Do not prefix with NEXT_PUBLIC_.OBH_SERVICE_KEY="svc_2f4a9c18e0d14b2a.example-secret-value"# Optional: set to "true" in local/test environments to use the OBH Sadad simulatorOBH_SIMULATOR="true"import { OBHPayments } from "@obhco/payments" // Requests automatically target https://obhco.ir/api/v1export const obh = new OBHPayments({ secretKey: process.env.OBH_SERVICE_KEY!, timeoutMs: 15_000,})The browser guard is not a secret manager
The SDK throws if it detects window, which catches accidents early. It cannot prevent a secret that was bundled for the client from being read. Store the key in server-only configuration.
3. Create the client
The constructor validates the credential prefix, the base URL, the timeout, and the fetch implementation immediately. Misconfiguration fails at startup instead of during a checkout.
import { OBHPayments } from "@obhco/payments" const obh = new OBHPayments({ secretKey: "svc_2f4a9c18e0d14b2a.example-secret-value",})// Uses https://obhco.ir/api/v1 by default and reads process.env.OBH_SIMULATORThe client is stateless apart from configuration, so you can reuse a single instance for the lifetime of the process. See Configuration for every option and the exact URL rules.
4. Create a payment
A payment needs an order id from your system, an amount, and a description. The return URL tells OBH where the customer should land after checkout.
import { NextResponse } from "next/server" import { obh } from "@/lib/obh" export async function POST(request: Request) { const form = await request.formData() const orderId = String(form.get("orderId")) const payment = await obh.startPayment({ externalOrderId: orderId, amount: "245000000", currency: "IRR", description: "Annual design workspace", returnUrl: "https://igraphical.ir/payments/return", metadata: { plan: "annual" }, // Persist this key with the order before sending the request. idempotencyKey: `order-${orderId}-attempt-1`, }) // Save payment.reference next to the order, then send the customer // to the provider's checkout page. await saveReference(orderId, payment.reference) if (payment.checkoutUrl) { return NextResponse.redirect(payment.checkoutUrl) } return NextResponse.json({ payment })}The response contains a reference that belongs to OBH, and usually a checkoutUrl. Some adapters complete the payment without a redirect, in which case checkoutUrl is null — always branch on it rather than assuming a redirect exists.
5. Verify the result
When the customer comes back, do not read the URL for a status. Look up the payment you stored and ask OBH.
import { notFound } from "next/navigation" import { obh } from "@/lib/obh" export default async function ReturnPage({ searchParams,}: { searchParams: Promise<{ reference?: string }>}) { const { reference } = await searchParams if (!reference) { notFound() } const payment = await obh.getPayment(reference) if (payment.status === "completed") { return <p>Thanks — your payment is confirmed.</p> } if (payment.status === "pending") { return <p>Still confirming. This page will update shortly.</p> } return <p>We could not confirm this payment ({payment.status}).</p>}Never fulfil on a redirect alone
A customer can edit a return URL, replay it, or close the browser before the provider confirms anything. Only status === "completed" from getPayment() (or a verified webhook followed by a retrieve) is proof of payment.
Checklist
Common mistakes
Importing the SDK in a client component
A file marked "use client" is built for the browser, so the constructor throws. Do the call in a route handler, server action, or a module marked server-only.
Using http:// in production
Only localhost, 127.0.0.1, and [::1] may use plain HTTP. Anything else must be https://.
Sending numbers instead of strings
Amounts are decimal strings: "245000000", not 245000000. This avoids floating-point rounding at the boundary.
Generating a new idempotency key on retry
If a request times out and you retry with a fresh key, OBH may create a second payment. Derive the key once and persist it. See Idempotency & retries.
Prefer a complete framework example? Read the Next.js integration guide.