H2H overview
Build a signed API-controlled checkout with typed actions, asynchronous verification, and webhooks.
Head-to-head (H2H) deposits use the existing
POST /api/v1/deposits endpoint with checkout_mode: "h2h". Omitting the
field preserves the redirect-checkout contract. An H2H response can either
return payment instructions for your UI or ask you to redirect the payer.
Always follow the returned next_action.
Integration sequence
- Create or select an API key and rotate its one-time H2H signing secret.
- Sign the exact create request and send it from your backend.
- Inspect
next_action.type. - Continue with the matching branch:
- For
display_instructions, show the returned details, collect the payer's transaction ID, and submit it tonext_action.verification.endpoint. - For
redirect, send the payer tonext_action.url. Do not call the manual verification endpoint.
- For
- Poll the signed deposit status while waiting, or process signed webhooks.
- Fulfill only after the top-level deposit
statusissucceeded.
Choose the next action
next_action | Your checkout behavior | Manual verification |
|---|---|---|
display_instructions | Render the returned recipient, operation, amount, and currency | Required through the returned verification.endpoint |
redirect | Open the returned url | Do not call /verify; monitor status and webhooks |
null | Stop presenting a payment action | Not available |
Do not choose an action from payment_method, and do not reconstruct payment
instructions. The response is the contract for that deposit. See
Next actions for the complete field-level behavior.
Minimal create request
{
"amount": "500.00",
"payment_method": "bkash_merchant",
"redirect_url": "https://merchant.example/return",
"merchant_id": "order-123",
"checkout_mode": "h2h",
"payer": {
"phone": "01712345678"
},
"metadata": {
"invoice_id": "INV-123"
}
}The request redirect_url is your return page—the destination the payer should
reach after a redirect checkout finishes. It is not the URL that starts
checkout. When the response has next_action.type: "redirect", send the payer
only to next_action.url.
H2H currently supports bkash, nagad, bkash_agent, nagad_agent,
bkash_merchant, and nagad_merchant. The amount must be a plain positive
decimal string with no more than two fractional digits and must be between BDT
100 and 25,000.
The payer phone must be a valid Bangladesh mobile number. OmniPay accepts local,
88…, and 880… forms and stores the normalized local form.
Example: display instructions
{
"id": "cm7deposit02",
"amount": "500.00",
"payment_method": "bkash_merchant",
"status": "pending",
"metadata": {
"invoice_id": "INV-123"
},
"merchant_id": "order-123",
"checkout_mode": "h2h",
"expires_at": "2026-07-28T12:05:00.000Z",
"next_action": {
"type": "display_instructions",
"channel": "bkash",
"operation": "make_payment",
"recipient": {
"type": "phone",
"value": "01800000000",
"name": "Example Merchant"
},
"qr_payload": null,
"amount": "500.00",
"currency": "BDT",
"verification": {
"endpoint": "/api/v1/deposits/cm7deposit02/verify",
"required_fields": ["provider_transaction_id"]
}
},
"verification": {
"state": "awaiting_reference",
"attempts_used": 0,
"attempts_remaining": 3
},
"provider_transaction_id": null
}Treat the recipient identity, display name, QR payload, amount, and currency
returned in next_action as authoritative for that deposit. These instructions
do not change after creation.
Example: redirect
{
"id": "cm7deposit03",
"amount": "500.00",
"payment_method": "bkash",
"status": "pending",
"metadata": {
"invoice_id": "INV-124"
},
"merchant_id": "order-124",
"checkout_mode": "h2h",
"expires_at": "2026-07-28T12:10:00.000Z",
"next_action": {
"type": "redirect",
"url": "https://pay.omnipay.page/checkout/session-123"
},
"verification": {
"state": "processing",
"attempts_used": 0,
"attempts_remaining": 0
},
"provider_transaction_id": null
}Send the payer to next_action.url before expires_at. Do not submit a
transaction ID for this action. After the payer returns to your redirect_url,
show a processing state while your backend reads the signed deposit status.
Treat the browser return as navigation only; fulfill from status: "succeeded"
or a verified deposit.succeeded webhook.
Handle the union exhaustively
const action = deposit.next_action;
if (action === null) {
showNonActionableState(deposit.status);
} else {
switch (action.type) {
case "display_instructions":
showPaymentInstructions(action);
break;
case "redirect":
window.location.assign(action.url);
break;
default:
assertNever(action);
}
}
function assertNever(value: never): never {
throw new Error(`Unsupported next action: ${JSON.stringify(value)}`);
}Keep fulfillment separate from action handling. A displayed instruction, successful browser return, or completed redirect is not proof of payment; only the signed deposit status or verified webhook is authoritative.
Availability
H2H must be enabled for your OmniPay organization. If an H2H request returns
403, contact your OmniPay account manager to confirm access.
Compatibility
Redirect checkout remains bearer-only and keeps its existing request and response contract. Adding H2H support does not change existing redirect transactions.