OBHPayments
↑↓ navigate↵ openSearches titles and topics
Documentation

Operations

Security

OBH keeps provider credentials and financial verification on the server. Your integration is responsible for a different half: keeping the service key server-side, not trusting the browser, and verifying webhooks.

Trust boundaries

Untrusted

Browser, return URL, query parameters, customer input, webhook payloads before verification

Your server

Service credential, order records, idempotency keys, webhook secret, fulfilment logic

OBH server

Provider credentials, provider accounts, verification, payment records, delivery workers

The SDK lives entirely in “your server”. It never exposes a browser entrypoint, and the service key must never cross into the “untrusted” zone.

Your responsibilities

  1. Keep the service key server-side. No NEXT_PUBLIC_ variables, no client bundles, no logs.
  2. Verify before fulfilling. Retrieve the payment and require completed.
  3. Fulfil idempotently. A return page, a webhook, and a retry can all race.
  4. Verify webhook signatures. With the webhook secret over the raw body, plus a timestamp tolerance.
  5. Deduplicate events. By OBH-Event-Id.
  6. Validate your own user input. The SDK validates the payment shape, not your authorization model.

Credentials

Credentials look like svc_test_<key-id>.<secret>. The key id is public; the secret is shown once at issuance. OBH stores only a SHA-256 digest and compares it in constant time.

  • Credentials are revocable; revocation applies on the next request.
  • TEST credentials are the only ones issued. A TEST key cannot produce LIVE financial records.
  • The credential identifies the service. It cannot be used to select a different originating service.
  • Rotate a credential if it may have leaked. There is no way to recover a secret.
Dots
// Server-only module, imported by route handlers and server components.import "server-only" export const obh = new OBHPayments({  secretKey: process.env.OBH_SERVICE_KEY!,  baseUrl: process.env.OBH_API_URL!,})
Do notts
// Exposed to the browser bundle. This key is compromised.const key = process.env.NEXT_PUBLIC_OBH_SERVICE_KEY // Logged on every request.console.log("auth", `Bearer ${process.env.OBH_SERVICE_KEY}`)

Redirects and return URLs

The return URL is a navigation hint, not a security control. OBH restricts redirects to an exact allowlist, which prevents open redirects — but it cannot make the resulting browser navigation trustworthy.

The classic mistake

A return page reads ?status=completed and grants access. An attacker can open that URL without paying. Always look up the stored reference and retrieve the status from OBH.

Register exact URLs, not wildcards or prefixes. The API rejects anything that is not an exact match with RETURN_URL_NOT_ALLOWED. Avoid putting sensitive data in the return URL; it is visible in browser history, referrers, and logs.

Metadata and customer input

Metadata is informational and visible to operators. It must never decide authorization, the amount, the currency, or the lifecycle. Treat it like a comment attached to the payment.

  • Maximum 8 KB when serialized.
  • Must be a JSON object, not an array or a scalar.
  • No secrets, tokens, or card data.
  • Validate and normalize it on your side before sending.

Amount and currency come from your server-side order record, not from the browser. A client-submitted price is a client-submitted discount.

Webhooks

The signing secret is per service and separate from the API credential. Verify HMAC-SHA256 over <timestamp>.<raw body> with a timing-safe comparison, reject stale timestamps, and deduplicate by event id.

The full algorithm and a working handler are in Webhooks. Two rules are worth repeating:

  • Read the raw body before parsing; re-serialization changes bytes.
  • Use timingSafeEqual, not ===.

Logging

Log thisNever log this
Error code, HTTP status, request idService secret or Authorization header
Your order id and payment referenceWebhook signing secret
Duration, retry attempt, outcomeFull request bodies with sensitive metadata
Event id for webhooksRaw signature of a valid request

Also be careful with third-party error trackers: request objects and headers are frequently captured by default. Scrub authorization and any svc_ values before reporting.

Threat model

RiskMitigation
Secret leaked to the browserServer-only modules, no public env prefix, SDK browser guard, review bundles.
Fulfilling an unpaid orderRetrieve the payment, require completed, ignore return URL state.
Double fulfilmentIdempotent database transition keyed by payment reference.
Double charge on retryStable idempotency key, persisted before the request.
Forged webhookVerify HMAC with the webhook secret and a timestamp window.
Replayed webhookDeduplicate event ids; make fulfilment idempotent.
Cross-service data accessOBH scopes every read and idempotency ledger to the authenticated service.
Price tamperingRead the amount from the server-side order, never from the client.
Open redirectExact return URL allowlist, enforced by the API.
Insecure transportHTTPS required; HTTP only on localhost development.

Security checklist

  • The SDK is imported only from server-only modules.
  • No secret is in a client bundle, log, or error tracker.
  • Fulfilment requires a retrieved completed status.
  • Fulfilment is idempotent on the payment reference.
  • Webhook HMAC and timestamp are verified before parsing.
  • Webhook event ids are deduplicated.
  • Return URLs are exact and allowlisted.
  • Amounts come from server-side order data.
  • Credentials can be revoked and rotated without downtime.