OBHPayments
↑↓ navigate↵ openSearches titles and topics
Documentation

Building

Check status

getPayment() reads the current state of one payment from OBH. It is the only value you should use to decide whether an order was paid.

Retrieve one payment

ts
class OBHPayments {  getPayment(reference: string): Promise<Payment>}
GET/api/v1/payments/{reference}
TypeScriptts
const payment = await obh.getPayment("OBH-8B30F9C07A52D1E4") console.log(payment.status)       // "pending" | "completed" | ...console.log(payment.completedAt)  // string | nullconsole.log(payment.verifiedAt)   // string | null

The method accepts a reference of 1–64 characters, URL-encodes it, and returns the same Payment shape as startPayment(). Retrieval has no side effects.

What counts as done

StatusWhat your application should do
completedFulfil the order exactly once. Persist the fact that you fulfilled it, keyed by payment reference.
pendingDo not fulfil. Show a waiting state, keep polling with backoff, or wait for the webhook.
failedDo not fulfil. Offer a retry that creates a new order attempt with a new idempotency key.
cancelledDo not fulfil. Let the customer start a new checkout.
expiredDo not fulfil. The checkout window closed; a new payment is required.

Fulfil at most once

A return page can be refreshed, a webhook retried, and a poller run twice. Make fulfilment idempotent on your side too — for example, a unique constraint on the payment reference, or an update that only transitions an order from unpaid to paid.

Polling with backoff

A payment is often pending for a few seconds after the customer returns. Poll with exponential backoff and a hard attempt limit; do not loop tightly or forever.

lib/wait-for-payment.tsts
import { obh } from "@/lib/obh"import type { Payment } from "@obhco/payments" const sleep = (ms: number) =>  new Promise((resolve) => setTimeout(resolve, ms)) export async function waitForPayment(  reference: string,  { attempts = 7, baseDelayMs = 800 } = {}): Promise<Payment> {  for (let attempt = 0; attempt < attempts; attempt += 1) {    const payment = await obh.getPayment(reference)     if (payment.status !== "pending") {      return payment    }     const exponential = baseDelayMs * 2 ** attempt    const capped = Math.min(exponential, 8_000)    const jitter = Math.random() * 250     await sleep(capped + jitter)  }   // Still pending: return the latest known state, do not invent one.  return obh.getPayment(reference)}

Backoff protects both services: it avoids rate-limit pressure and it gives the provider time to settle. Jitter prevents many customers returning at the same moment from producing a synchronized burst.

Serverless time limits

A request that waits 60 seconds may exceed your platform’s execution limit. Prefer a webhook to trigger reconciliation, or move long waits into a background worker. A pending return page can also refresh itself and poll a lightweight endpoint.

Webhook-first reconciliation

The recommended production pattern is:

  1. Create the payment and store its reference.
  2. Redirect the customer to checkout.
  3. Receive a payment.completed or payment.failed webhook on your server.
  4. Verify the signature, deduplicate the event id, then retrieve.
  5. Fulfil only if the retrieved status is completed.
  6. Use the return page as a UX shortcut that does the same retrieval — never as the only path.

Webhooks are covered in detail in Webhooks.

Reconcile after a webhookts
async function reconcile(reference: string) {  const payment = await obh.getPayment(reference)   if (payment.status === "completed") {    await markOrderPaidOnce(payment.externalOrderId!)  } else if (payment.status !== "pending") {    await markOrderFailedOnce(payment.reference, payment.status)  }}

References and isolation

A reference looks like OBH-8B30F9C07A52D1E4. It identifies a payment within OBH and is safe to store and to show to the customer as an order code.

  • A service can only read its own payments. Another service’s reference returns 404 PAYMENT_NOT_FOUND, exactly like a reference that does not exist. This prevents enumerating other services’ payments.
  • The reference is not a secret, but it is also not an authorization token. Anyone with it can only read the payment through an authenticated service call.
  • Do not confuse reference with your own externalOrderId. OBH creates the reference; you create the order id.

Failure modes

SituationWhat happensWhat to do
Payment not found404 PAYMENT_NOT_FOUNDCheck the reference and that you are using the owning service’s key.
Credential revoked401 AUTH_INVALIDIssue a new credential; retrying will not help.
API unreachableNETWORK_ERRORRetry the read with backoff. Reads are always safe to retry.
Took too longTIMEOUTRetry the read; the payment state is unaffected.
Malformed responseINVALID_RESPONSETreat as an integration bug. Record the requestId and contact support.

Next, learn how to make creation retries safe: Idempotency & retries.