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
- Keep the service key server-side. No
NEXT_PUBLIC_variables, no client bundles, no logs. - Verify before fulfilling. Retrieve the payment and require
completed. - Fulfil idempotently. A return page, a webhook, and a retry can all race.
- Verify webhook signatures. With the webhook secret over the raw body, plus a timestamp tolerance.
- Deduplicate events. By
OBH-Event-Id. - 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.
// 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!,})// 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 this | Never log this |
|---|---|
| Error code, HTTP status, request id | Service secret or Authorization header |
| Your order id and payment reference | Webhook signing secret |
| Duration, retry attempt, outcome | Full request bodies with sensitive metadata |
| Event id for webhooks | Raw 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
| Risk | Mitigation |
|---|---|
| Secret leaked to the browser | Server-only modules, no public env prefix, SDK browser guard, review bundles. |
| Fulfilling an unpaid order | Retrieve the payment, require completed, ignore return URL state. |
| Double fulfilment | Idempotent database transition keyed by payment reference. |
| Double charge on retry | Stable idempotency key, persisted before the request. |
| Forged webhook | Verify HMAC with the webhook secret and a timestamp window. |
| Replayed webhook | Deduplicate event ids; make fulfilment idempotent. |
| Cross-service data access | OBH scopes every read and idempotency ledger to the authenticated service. |
| Price tampering | Read the amount from the server-side order, never from the client. |
| Open redirect | Exact return URL allowlist, enforced by the API. |
| Insecure transport | HTTPS 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
completedstatus. - 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.