OBHPayments
↑↓ navigate↵ openSearches titles and topics
Documentation

Building

Create a payment

startPayment() creates a payment record, asks OBH to open a provider session, and returns everything you need to send the customer to checkout. It is safe to retry when you use a stable idempotency key.

Signature

ts
class OBHPayments {  startPayment(input: StartPaymentInput): Promise<Payment>}
POST/api/v1/payments

The method validates the input locally, adds your credential and an idempotency header, retries nothing on its own, and resolves to the created (or replayed) payment.

Inputs

PropertyTypeDescription
externalOrderIdrequiredstringYour order identifier. 1–191 characters and unique per service. Echoed back on every payment.
amountrequiredstringPositive integer string in Iranian Rials (IRR), 1–18 digits. Never a JavaScript number.
currencyoptional
default: "IRR"
"IRR"The only supported currency. Any other value is rejected locally.
descriptionrequiredstringWhat the customer is paying for. 1–500 characters after trimming; shown in dashboards and receipts.
returnUrloptionalstringAbsolute http(s) URL the customer returns to. Must match the service allowlist exactly. Defaults to the service’s configured return URL.
metadataoptionalRecord<string, unknown>Informational JSON object, serialized to at most 8 KB. Never used for authorization or amounts.
gatewayoptional
default: "sadad"
"sadad" | "zarinpal" | "mellat" | "digipay"The gateway the customer chose at checkout. Use a code from listGateways(). See Gateways & installments.
customerMobileoptionalstringCustomer mobile number such as 09121234567. Required when the gateway has requiresCustomerMobile (Digipay).
idempotencyKeyoptionalstring1–191 visible ASCII characters. Reuse it when retrying the same request. Generated as sdk_<uuid> when omitted.
simulatoroptional
default: process.env.OBH_SIMULATOR === 'true'
booleanWhen true, routes the payment to the OBH gateway simulators instead of the live gateways.

Validation rules

The SDK validates before making a network request, so most mistakes surface as an INVALID_ARGUMENT error without touching the API. The API validates again — never assume the local checks are the full contract.

FieldRule enforced by the SDK
externalOrderIdNon-empty, at most 191 characters.
amountMatches ^\d{1,18}$ (whole Iranian Rials) and is greater than zero. Leading zeros, decimals, and "0" are rejected.
currencyMust be "IRR" when provided.
descriptionNon-empty after trimming, at most 500 characters.
returnUrlMust parse as an absolute URL with an http: or https: protocol. Allowlisting happens on the API.
metadataMust be a plain JSON object (not an array, not null), at most 8 KB when serialized.
gatewayLowercase letters, digits, _ or -, at most 40 characters. Whether OBH offers it is checked by the API.
customerMobileAn Iranian mobile number: 09…, +989… or 989….
idempotencyKey1–191 printable ASCII characters (no spaces or control characters).
Valid and invalid inputsts
// Validawait obh.startPayment({  externalOrderId: "IG-8821",  amount: "245000000",  description: "Annual design workspace",}) // Invalid: amount is a numberawait obh.startPayment({ amount: 245000000 /* ... */ }) // Invalid: zeroawait obh.startPayment({ amount: "0" /* ... */ }) // Invalid: decimals (IRR amounts are whole integers)await obh.startPayment({ amount: "10.99" /* ... */ })

Return URLs

The return URL is where the provider sends the customer after checkout. OBH only redirects to an exact match from the service allowlist; there is no wildcard or prefix matching.

  • If you omit returnUrl, the service’s default is used.
  • If neither exists, the API fails with RETURN_URL_REQUIRED.
  • If the URL is not on the allowlist, the API fails with RETURN_URL_NOT_ALLOWED.

Use a URL on your own origin and verify the payment server-side when it loads. Never put a status or amount in the return URL and trust it when it comes back.

Which return URL should I send?

Send the exact absolute URL you registered, including protocol and trailing slash behavior. A mismatch in either is a mismatch.

Metadata

Metadata is a small JSON object attached to the payment for your own reference — a plan name, an internal user id, a campaign, or a correlation id. It is informational only.

ts
metadata: {  plan: "annual",  seats: 12,  campaign: "autumn-2026",  internalUserId: "usr_9f2c",}

Never put secrets in metadata

Metadata is stored and visible to OBH operators and support tooling. Do not include credentials, access tokens, card data, or anything you would not put in an email. It must never influence authorization, amount, currency, or lifecycle decisions.

Idempotency

Every create request carries an Idempotency-Key header. If you provide one, the SDK uses it verbatim. If you omit it, the SDK generates sdk_<uuid> — fine for a single attempt, but useless for a retry, because the next call gets a different key.

ts
const payment = await obh.startPayment({  externalOrderId: order.id,  amount: order.total,  description: order.title,  // Stable for this attempt, persisted before the request.  idempotencyKey: `order-${order.id}-attempt-${attempt}`,})

Reusing a key with an identical request returns the original payment. Reusing it with a different request returns 409 IDEMPOTENCY_CONFLICT. The full rules and retry strategy live in Idempotency & retries.

What you get back

A resolved promise is a Payment object. The fields you will use most:

Paymentjson
{  "id": "018f0000-0000-7000-8000-000000000001",  "reference": "OBH-8B30F9C07A52D1E4",  "service": "igraphical",  "externalOrderId": "IG-8821",  "amount": "245000000",  "currency": "IRR",  "environment": "live",  "gateway": "sadad",  "status": "pending",  "description": "Annual design workspace",  "checkoutUrl": "https://sadad.shaparak.ir/VPG/Purchase?Token=...",  "receipt": {    "orderNumber": "1042",    "traceNumber": null,    "referenceNumber": null,    "maskedCardNumber": null,    "resultCode": "0",    "resultMessage": null  },  "createdAt": "2026-10-18T08:00:00.000Z",  "completedAt": null,  "verifiedAt": null}

Three details matter:

  • Store reference now. It is how you read the payment later. Without it, you cannot recover the result.
  • checkoutUrl may be null. Some adapters do not use a browser redirect. Do not dereference it blindly.
  • The amount is returned in whole Iranian Rials (IRR). OBH returns the integer string (e.g. "245000000").

The SDK does not expose the HTTP status

OBH returns 201 for a new payment and 200 for an idempotent replay, with an Idempotent-Replayed header. The SDK normalizes both into the same Payment value. If you need to distinguish them, call the HTTP API directly.

Complete example

app/actions/checkout.tsts
"use server" import { redirect } from "next/navigation" import { obh } from "@/lib/obh"import { db } from "@/lib/db" export async function checkout(orderId: string) {  const order = await db.order.findUniqueOrThrow({    where: { id: orderId },  })   // A stable key per order attempt. If this key already exists,  // OBH returns the original payment instead of creating another.  const idempotencyKey = `order-${order.id}-attempt-${order.attempt}`   const payment = await obh.startPayment({    externalOrderId: order.number,    amount: order.total,    currency: "IRR",    description: order.title,    returnUrl: "https://igraphical.ir/payments/return",    metadata: { orderId: order.id, plan: order.plan },    idempotencyKey,  })   await db.order.update({    where: { id: order.id },    data: {      paymentReference: payment.reference,      paymentStatus: payment.status,    },  })   if (!payment.checkoutUrl) {    redirect(`/payments/return?reference=${payment.reference}`)  }   redirect(payment.checkoutUrl)}

Errors from creation

Creation can fail before or during the request. The most common codes are listed here; the full table is in Errors.

CodeTypical causeRetry?
INVALID_ARGUMENTLocal validation failed — bad amount, URL, or metadata.Only after fixing the input.
VALIDATION_ERRORThe API rejected a field the SDK did not pre-check.Only after fixing the input.
ORDER_CONFLICTThe external order id exists with different financial details.No. Reconcile the existing payment.
IDEMPOTENCY_CONFLICTThe same key was used for a different request.No. Use a new key for a new request.
RETURN_URL_NOT_ALLOWEDThe return URL is not in the exact allowlist.No. Register or correct the URL.
GATEWAY_NOT_SUPPORTEDThe gateway code is not one OBH offers.No. Pick a gateway from listGateways().
CUSTOMER_MOBILE_REQUIREDThe chosen gateway (Digipay) needs customerMobile.Only after adding the mobile number.
GATEWAY_NOT_AVAILABLEThe gateway is not enabled for LIVE payments yet.No. Offer another gateway from listGateways().
PROVIDER_UNAVAILABLENo active provider account or adapter for the environment.Yes, with backoff — and only with the same key.
TIMEOUT / NETWORK_ERRORThe request never produced a definitive answer.Retrieve first, then retry with the same key if needed.

Timeout does not mean failure

A timeout means you do not know whether OBH created the payment. It may exist and be pending. Call getPayment(reference) if you have the reference, or retry the create with the same idempotency key — never with a fresh one.