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
class OBHPayments { getPayment(reference: string): Promise<Payment>}/api/v1/payments/{reference}const payment = await obh.getPayment("OBH-8B30F9C07A52D1E4") console.log(payment.status) // "pending" | "completed" | ...console.log(payment.completedAt) // string | nullconsole.log(payment.verifiedAt) // string | nullThe 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
| Status | What your application should do |
|---|---|
| completed | Fulfil the order exactly once. Persist the fact that you fulfilled it, keyed by payment reference. |
| pending | Do not fulfil. Show a waiting state, keep polling with backoff, or wait for the webhook. |
| failed | Do not fulfil. Offer a retry that creates a new order attempt with a new idempotency key. |
| cancelled | Do not fulfil. Let the customer start a new checkout. |
| expired | Do 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.
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:
- Create the payment and store its reference.
- Redirect the customer to checkout.
- Receive a
payment.completedorpayment.failedwebhook on your server. - Verify the signature, deduplicate the event id, then retrieve.
- Fulfil only if the retrieved status is
completed. - 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.
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
referencewith your ownexternalOrderId. OBH creates the reference; you create the order id.
Failure modes
| Situation | What happens | What to do |
|---|---|---|
| Payment not found | 404 PAYMENT_NOT_FOUND | Check the reference and that you are using the owning service’s key. |
| Credential revoked | 401 AUTH_INVALID | Issue a new credential; retrying will not help. |
| API unreachable | NETWORK_ERROR | Retry the read with backoff. Reads are always safe to retry. |
| Took too long | TIMEOUT | Retry the read; the payment state is unaffected. |
| Malformed response | INVALID_RESPONSE | Treat as an integration bug. Record the requestId and contact support. |
Next, learn how to make creation retries safe: Idempotency & retries.