Skip to content

This chapter is the canonical reference for accepting deposits through the Payment Gateway merchant API. It covers both deposit modes (Direct API and Cashier session), their state machines, the exact request and response shapes, and the pitfalls that show up in production.

Concepts and terminology

TermMeaning
Deposit orderA DepositOrder row that represents a single payment intent from one of your end users. Identified by order_no (assigned by the gateway) and merchant_order_no (assigned by you).
ChannelAn upstream payment provider (for example, a third-party PSP or a virtual-account aggregator). A channel order is routed to a PaymentChannel and exposes a payment_url or channel-supplied payment_info.
AccountA physical bank account owned by the operator. An account order asks the payer to send funds to that account; matching is done by expected_remark or amount float.
payment_urlA redirect URL returned by some channels. Send the payer here to complete payment. Populated only when the order is routed to a channel that uses a hosted payment page.
payment_infoA channel-supplied JSON object containing bank details, QR code data, or any other payer-facing instructions. Populated for channel orders that do not use a redirect URL.
Cashier sessionA CashierSession row that represents a hosted, payer-facing page. Without amount, the payer picks the amount on this page and the actual DepositOrder is created after the payer submits. When the request includes amount, the DepositOrder is created immediately with the session and the hosted page opens directly on payment instructions.
PayerThe end user sending funds. Distinct from the merchant. The payer interacts with payment_url, payment_info, or the hosted cashier page. The payer never authenticates against the merchant API.
MATCHED stateThe deposit has been recognised as paid (a bank transaction matched the order, or the channel reported success), but settlement and ledger commit have not finished. Not a terminal state.
Mock channelA built-in fake channel available in the sandbox. Use it to drive the state machine end-to-end without involving a real PSP. See Sandbox.

Two deposit modes

AspectDirect APICashier session
EndpointPOST /api/v1/merchant/orders/depositPOST /api/v1/merchant/orders/deposit/cashier
Who picks the amountThe merchant, in the request body.Either the payer on the hosted page, or the merchant when amount is provided in the cashier-session request.
Who picks the channelThe gateway routing engine, based on currency and amount.The gateway routing engine after the payer submits, or immediately when the cashier-session request includes amount.
Initial responseDepositPaymentInfo with payment_url or bank details.CashierCreateResponse with cashier_url and cashier_token.
When DepositOrder row is createdImmediately on POST /deposit.On payer submit by default. If the cashier request includes amount, it is created immediately with the session and the hosted page opens directly on payment instructions.
What the merchant displays to the payerYour own UI; you render the bank details or redirect to payment_url.A redirect to cashier_url. The gateway hosts everything.
order_type on the resulting orderDIRECTCASHIER

Decision guide

Use Direct API if you already collected the amount, currency, and payer intent in your own UI. Your application stays in control of the checkout look and feel. You handle the redirect or the bank-details screen yourself.

Use Cashier session if you want Payment Gateway to host the payer-facing screen. Omit amount when the payer should pick from the hosted denomination selector. Provide amount when you already collected the amount and only want the hosted page to display the payment instructions. You hand the payer a single cashier_url and stop touching the flow until you receive the callback.

Both modes converge on the same DepositOrder entity and the same callback handshake. Querying, callback verification, and state semantics are identical.

API surface

MethodPathPurpose
POST/api/v1/merchant/orders/depositCreate a Direct API deposit order.
POST/api/v1/merchant/orders/deposit/cashierCreate a Cashier session.
GET/api/v1/merchant/query/deposit/{order_no}Query a deposit order by gateway order_no.
GET/api/v1/merchant/query/deposit?merchant_order_no=...Query a deposit order by your merchant_order_no.

All endpoints require the standard MD5 signature headers (lowercase-hex MD5 over sorted params + API key). See Authentication.

DepositOrderCreate (Direct API request)

FieldTypeRequiredNotes
merchant_order_nostring (1-64)yesYour unique reference for this deposit. Must be unique per merchant.
amountdecimalyesMust be greater than 0. Subject to currency precision validation.
currency_codestring (3)yesISO 4217 code. Must be a currency your merchant account is enabled for.
callback_urlstring (1-500)yesHTTPS URL we call on status changes. Required for Direct API.
extra_datastringnoOpaque string echoed back in the callback.
user_idstring (≤64)noYour internal user identifier. Used for grouping and audit.

CashierDepositOrderCreate (Cashier session request)

FieldTypeRequiredNotes
order_typeenumnoDefaults to cashier. The field exists for forward compatibility.
merchant_order_nostring (1-64)yesSame uniqueness rules as Direct API.
amountdecimalnoOptional pre-filled amount. Must be greater than 0 and must match a configured cashier denomination, unless custom cashier amounts are enabled and the value is within the configured min/max range. When present, the gateway creates the DepositOrder immediately and the hosted page skips denomination selection.
currency_codestring (3)yesISO 4217 code.
callback_urlstring (≤500)noOptional for Cashier. If omitted, the merchant default is used.
extra_datastringnoEchoed back in the callback.
user_idstring (≤64)noStrongly recommended. The gateway reuses an existing PENDING session for the same user_id, which prevents accidental duplicate sessions if your application retries.
payer_bank_cardslist of objects (≤10)noPre-bound payer cards. When provided, the cashier page forces the payer to pick one.

DepositPaymentInfo (Direct API response, HTTP 201)

FieldPopulated whenNotes
order_noalwaysGateway-assigned identifier. Use this in queries.
requested_amountalwaysThe amount you sent. May differ from amount.
amountalwaysThe amount the payer must transfer. Differs from requested_amount when the routing engine applies amount float for matching.
currency_codealwaysEchoed from the request.
expire_atalwaysRFC 3339 timestamp. Default expiry is 15 minutes; configurable per merchant.
payment_urlchannel orders onlyRedirect target for the payer.
payment_infochannel orders onlyChannel-supplied JSON. Shape varies by channel.
bank_code, bank_name, bank_branch, account_no, account_holderaccount orders, and convenience-mirrored for channel orders that returned bank detailsBank details to display to the payer.
remarkaccount orders onlyThe string the payer must put in the bank transfer memo. The matcher uses this.

CashierCreateResponse (Cashier response, HTTP 201)

FieldNotes
cashier_urlFull URL to redirect the payer to. Contains the signed token.
cashier_tokenSame token also exposed for back-channel inspection. Not strictly required for the merchant.
session_idCashierSession.id. Persist for logging.
expire_atSession expiry. Defaults to 15 minutes, configurable per merchant.

Full schema definitions, including all response fields on DepositOrderResponse, are in the API reference.

Direct API flow

The diagram captures the happy path. The gateway routes the order at step 2-3 based on configured routing rules: a PaymentChannel if one matches, otherwise a managed bank Account. The order row is committed with status=PENDING and expire_at set from the merchant's deposit_expire_minutes config (default 15 minutes).

When the payer pays, the gateway moves the order through MATCHED (payment recognised) to COMPLETED (ledger settled), then fires the callback. Failure branches (FAILED, EXPIRED, CANCELLED) are detailed in State transitions.

Cashier session flow

Key points specific to Cashier mode:

  • Without amount, the DepositOrder row does not exist until the payer submits. Up to that point you only have a CashierSession. Querying /api/v1/merchant/query/deposit/{order_no} will return 404 because there is no order_no yet.
  • With amount, the DepositOrder is created immediately. The cashier session is returned as submitted, has_deposit_order=true on the public cashier API, and the hosted page skips the denomination selector.
  • The signed token in cashier_url is short-lived and tied to the session. It is not a merchant API credential. Do not try to call merchant API endpoints with it.
  • Session reuse is keyed on (merchant_id, user_id). If a PENDING session exists for the same user, the gateway updates it with the new merchant_order_no, currency, and callback URL and returns the same token. This prevents accidental duplicate sessions on retries.
  • expire_at on the session is independent of expire_at on the eventual order. When the payer submits, the order gets a fresh expiry from the merchant config.
  • Cashier pages may use WebSocket status updates to push state changes to the payer's browser without polling. This is transparent to the merchant integration: you still rely on the server-to-server callback.

Worked examples

The snippets below assume you have generated X-Timestamp and X-Signature headers as documented in Authentication. Sign all four samples with the same key:

bash
#!/usr/bin/env bash
# MD5 signature for Payment Gateway merchant API (bash reference).
#
# Usage:
#   ./sign.sh --api-key <key> --params 'k1=v1&k2=v2&...'
set -euo pipefail

API_KEY=""
PARAMS=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --api-key) API_KEY="$2"; shift 2 ;;
    --params)  PARAMS="$2";  shift 2 ;;
    *) echo "Unknown flag: $1" >&2; exit 2 ;;
  esac
done

# Sort key=value pairs ASCII order (C locale), drop empty trailing field
SORTED=$(printf '%s' "$PARAMS" | tr '&' '\n' | LC_ALL=C sort | grep -v '^$' | tr '\n' '&' | sed 's/&$//')
SIGN_STRING="${SORTED}${API_KEY}"

# MD5: md5sum on Linux/CI, md5 on macOS
if command -v md5sum >/dev/null 2>&1; then
  printf '%s' "$SIGN_STRING" | md5sum | cut -d' ' -f1
else
  printf '%s' "$SIGN_STRING" | md5
fi
python
#!/usr/bin/env python3
"""Reference implementation: MD5 signature for Payment Gateway merchant API.

Usage:
  python sign.py --api-key <key> --params 'k1=v1&k2=v2&...'

Algorithm:
  1. Parse params into a dict.
  2. Sort keys alphabetically (ASCII order).
  3. Build query string 'k=v&k=v', skipping None values.
  4. Append api_key directly (no '&').
  5. MD5 the UTF-8 bytes; output lowercase hex.
"""
import argparse
import hashlib
import sys
from urllib.parse import parse_qsl


def create_signature(params: dict[str, str], api_key: str) -> str:
    sorted_items = sorted(params.items())
    query_string = "&".join(f"{k}={v}" for k, v in sorted_items if v is not None)
    return hashlib.md5((query_string + api_key).encode("utf-8")).hexdigest()


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--api-key", required=True)
    parser.add_argument("--params", required=True)
    args = parser.parse_args()
    params = dict(parse_qsl(args.params, keep_blank_values=True))
    print(create_signature(params, args.api_key))
    return 0


if __name__ == "__main__":
    sys.exit(main())
php
<?php
/**
 * MD5 signature for Payment Gateway merchant API (PHP reference).
 *
 * Usage:
 *   php sign.php --api-key=<key> --params='k1=v1&k2=v2&...'
 *
 * Note: getopt() requires --flag=value form (no space between flag and value).
 */
$opts = getopt('', ['api-key:', 'params:']);
if (!isset($opts['api-key']) || !isset($opts['params'])) {
    fwrite(STDERR, "Usage: php sign.php --api-key=<key> --params='k=v&...'\n");
    exit(2);
}

parse_str($opts['params'], $params);
ksort($params, SORT_STRING);

$parts = [];
foreach ($params as $k => $v) {
    if ($v === null) continue;
    $parts[] = "$k=$v";
}
echo md5(implode('&', $parts) . $opts['api-key']);
typescript
#!/usr/bin/env node
/**
 * MD5 signature for Payment Gateway merchant API (Node.js / TypeScript reference).
 *
 * Usage:
 *   tsx sign.ts --api-key <key> --params 'k1=v1&k2=v2&...'
 */
import { createHash } from "crypto";

function parseArgs(argv: string[]): { apiKey: string; params: string } {
  let apiKey = "", params = "";
  for (let i = 2; i < argv.length; i++) {
    if (argv[i] === "--api-key") apiKey = argv[++i];
    else if (argv[i] === "--params") params = argv[++i];
  }
  if (!apiKey || !params) {
    process.stderr.write("Usage: sign.ts --api-key <key> --params 'k=v&...'\n");
    process.exit(2);
  }
  return { apiKey, params };
}

function createSignature(paramsQuery: string, apiKey: string): string {
  const parsed = new Map<string, string>();
  for (const pair of paramsQuery.split("&")) {
    if (!pair) continue;
    const eq = pair.indexOf("=");
    const k = eq >= 0 ? pair.slice(0, eq) : pair;
    const v = eq >= 0 ? pair.slice(eq + 1) : "";
    parsed.set(k, v);
  }
  const sorted = [...parsed.entries()].sort(([a], [b]) =>
    a < b ? -1 : a > b ? 1 : 0
  );
  const query = sorted.map(([k, v]) => `${k}=${v}`).join("&");
  return createHash("md5").update(query + apiKey, "utf8").digest("hex");
}

const { apiKey, params } = parseArgs(process.argv);
process.stdout.write(createSignature(params, apiKey));

In each example the --data payload is the JSON body that you sign. Sign the body, not the URL.

Example 1: Direct API deposit

Create a direct deposit. The gateway chooses the channel or account; you cannot pin a channel from the merchant API in this version.

bash
curl -X POST https://pre-api.open-pay.co/api/v1/merchant/orders/deposit \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -d '{
    "merchant_order_no": "DEP-20260520-0001",
    "amount": "1000.00",
    "currency_code": "THB",
    "callback_url": "https://merchant.example.com/api/pg/callback",
    "user_id": "user-42",
    "extra_data": "checkout-cart-77"
  }'

Response (channel-routed, abbreviated):

json
{
  "order_no": "D2026052012345678",
  "requested_amount": "1000.00",
  "amount": "1000.00",
  "currency_code": "THB",
  "expire_at": "2026-05-20T08:15:00Z",
  "payment_url": "https://pay.example-psp.com/pay/abc123",
  "payment_info": {
    "bank_code": "SCB",
    "bank_name": "Siam Commercial Bank",
    "account_no": "1234567890",
    "account_name": "OPEN PAY CO LTD"
  },
  "bank_code": "SCB",
  "bank_name": "Siam Commercial Bank",
  "account_no": "1234567890",
  "account_holder": "OPEN PAY CO LTD"
}

Persist order_no against your merchant_order_no. You will need it for queries and callback reconciliation.

Example 2: Cashier session

Create a cashier session and send the payer to cashier_url.

bash
curl -X POST https://pre-api.open-pay.co/api/v1/merchant/orders/deposit/cashier \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -d '{
    "merchant_order_no": "DEP-20260520-0002",
    "currency_code": "THB",
    "user_id": "user-42",
    "callback_url": "https://merchant.example.com/api/pg/callback"
  }'

Response:

json
{
  "cashier_url": "https://pre-cashier.open-pay.co/?token=eyJhbGciOi...",
  "cashier_token": "eyJhbGciOi...",
  "session_id": "0a3f5c2e-1c0b-4a3b-9c2d-1f4e7a8b9c0d",
  "expire_at": "2026-05-20T08:15:00Z"
}

Redirect the payer's browser to cashier_url. Do not embed the page in a hidden iframe; the cashier page expects a top-level browser context.

Example 3: Cashier session with a pre-filled amount

Use this mode when your own checkout already collected the amount, but you still want Payment Gateway to host the payment-instructions page. The amount must pass the same denomination/custom-amount validation as the hosted selector.

bash
curl -X POST https://pre-api.open-pay.co/api/v1/merchant/orders/deposit/cashier \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -d '{
    "merchant_order_no": "DEP-20260520-0003",
    "amount": "1000.00",
    "currency_code": "THB",
    "user_id": "user-42",
    "callback_url": "https://merchant.example.com/api/pg/callback"
  }'

Response:

json
{
  "cashier_url": "https://pre-cashier.open-pay.co/?token=eyJhbGciOi...",
  "cashier_token": "eyJhbGciOi...",
  "session_id": "1d0f5c2e-2c0b-4a3b-9c2d-1f4e7a8b9c0d",
  "expire_at": "2026-05-20T08:15:00Z"
}

Redirect the payer to cashier_url. The hosted page loads payment information directly instead of showing the denomination selector.

Example 4: Query a deposit after creation

Query by gateway order_no:

bash
curl https://pre-api.open-pay.co/api/v1/merchant/query/deposit/D2026052012345678 \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG"

Or by your merchant_order_no:

bash
curl "https://pre-api.open-pay.co/api/v1/merchant/query/deposit?merchant_order_no=DEP-20260520-0001" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG"

Both return the full DepositOrderResponse schema, including status, amount, actual_amount, matched_at, completed_at, and callback_status. Use the response to reconcile against your internal record.

Example 5: Duplicate merchant_order_no

If you call POST /deposit (or POST /deposit/cashier) with a merchant_order_no that already exists for your merchant, the backend rejects the request:

json
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "success": false,
  "code": "CONFLICT",
  "message": "Duplicate merchant order number: DEP-20260520-0001",
  "data": null
}

Recommended recovery: do not retry blindly. Query first.

bash
curl "https://pre-api.open-pay.co/api/v1/merchant/query/deposit?merchant_order_no=DEP-20260520-0001" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG"

If the existing order is already COMPLETED you are done. If it is still PENDING and not expired, surface its payment_url or bank details to the payer instead of creating a new order.

State transitions

DepositOrder state machine

From → ToTriggerMerchant observes
PENDING → MATCHEDA bank webhook matches the order's expected_remark or floated amount, or the channel sends a paid notification. Backend sets matched_at.No callback yet. Status visible via query.
MATCHED → COMPLETEDThe ledger commits the funds to the merchant balance. Backend sets completed_at, actual_amount.Signed callback fired. This is the only callback for a successful deposit.
PENDING → EXPIREDexpire_at has passed and no payment matched. A background job moves the row. The order does not auto-cancel; it explicitly transitions to EXPIRED and stops accepting payments.No callback for expiry by default. Reconcile via query.
PENDING → CANCELLED / MATCHED → CANCELLEDOperator or merchant explicitly cancels. For account orders the reserved deposit quota is released.No callback for cancel. Reconcile via query or operator notification.
PENDING → FAILED / MATCHED → FAILEDChannel reports a terminal failure (for example, the channel order was rejected by the upstream bank).Callback may or may not fire depending on channel. Reconcile via query.

MATCHED is not final

A MATCHED order can still move to CANCELLED or FAILED in rare cases (post-match channel errors, operator intervention). Only treat COMPLETED as final-success and EXPIRED / CANCELLED / FAILED as final-failure.

CashierSession state machine

From → ToTriggerMerchant observes
PENDING → SUBMITTEDFor sessions without amount, the payer picks a denomination (and bank card, if required) on the cashier page. Backend creates the DepositOrder and links it via deposit_order_id.No callback at this step. The deposit lifecycle begins now.
Created as SUBMITTEDThe cashier-session request includes amount. Backend creates the DepositOrder immediately, links it via deposit_order_id, and the hosted page skips denomination selection.No callback at this step. The merchant can query the resulting DepositOrder immediately.
PENDING → EXPIREDThe session's expire_at has passed and the payer never submitted. The next request that touches the session marks it expired; an explicit operator action can also move it.No callback. The session cannot be resumed; create a new one.

There is no SUBMITTED → EXPIRED path. Once a session is submitted, the associated DepositOrder carries its own state machine; the session is effectively a closed record.

Pitfalls

  1. Reusing merchant_order_no across different deposit intents. The backend rejects this with HTTP 409 and CONFLICT code in the standard error envelope. This now correctly uses HTTP 409. Generate a fresh merchant_order_no for each new deposit.

  2. Using a fresh merchant_order_no per retry. The opposite of pitfall 1. On a network blip during POST /deposit, if you retry with a new merchant_order_no, the first request might still have landed, leaving you with two orders. Always retry with the same merchant_order_no; the duplicate check then surfaces the conflict and you can query the existing order. See CONFLICT and BUSINESS_ERROR.

  3. Cashier session expires before the payer completes. Sessions cannot be resumed. The merchant must call POST /deposit/cashier again and redirect the payer to the new cashier_url. Set expire_at expectations on your end-user UI.

  4. Forgetting to verify the callback signature. Every callback is signed with the same MD5-over-sorted-params scheme as outbound requests. Skipping verification opens you to forged callbacks and breaks idempotency assumptions. See Callbacks and INVALID_SIGNATURE.

  5. Mixing Direct API and Cashier mode for the "same" order. Direct API and Cashier produce different DepositOrderType values (DIRECT vs CASHIER). Even with the same merchant_order_no they are not interchangeable. Pick one mode per logical deposit attempt.

  6. Branching on payment_url presence to detect Cashier mode. This is wrong. Cashier sessions return cashier_url, not payment_url. Direct API channel orders return payment_url. Direct API account orders return neither. Branch on which endpoint you called, or on order_type from the query response.

  7. Treating MATCHED as a final state. MATCHED means "the payment is recognised, the ledger has not committed yet." It can still move to CANCELLED or (rarely) FAILED. The signed callback fires at COMPLETED, not at MATCHED. See State transitions.

  8. Assuming amount equals requested_amount. For currencies and channels that use amount-float matching, the gateway adjusts the payable amount to disambiguate concurrent transfers to the same bank account. Honour amount (what the payer pays) over requested_amount (what you asked for) when displaying instructions. The fields are clearly named in DepositOrderResponse and DepositPaymentInfo.

  9. Ignoring expire_at. Expired orders move to DepositOrderStatus.EXPIRED and stop accepting payments. The payer's transfer arriving after expiry will not auto-match the order and may need manual reconciliation. See BUSINESS_ERROR for the response shape on operations against expired orders.

  10. Hard-coding bank account info in your UI. The payment_info, bank_code, bank_name, account_no, and account_holder fields on DepositPaymentInfo change per order — the routing engine may pick different accounts based on load, currency, or float availability. Render whatever the response returns; never pre-fill from your own database.

  11. Polling /query/deposit/ instead of relying on callbacks. Polling wastes your API budget and adds latency. The callback fires once at COMPLETED. Use GET /query/deposit/{order_no} only as a reconciliation fallback after a missed callback. See Callbacks for retry semantics.

  12. Not handling INSUFFICIENT_MERCHANT_BALANCE on accounts with credit-style configuration. Rare for deposits, but possible if your account has prepaid fee deduction enabled. See INSUFFICIENT_MERCHANT_BALANCE.

  13. Re-querying a Cashier session by merchant_order_no before submission. The GET /query/deposit?merchant_order_no=... endpoint queries DepositOrder, not CashierSession. A PENDING cashier session has no DepositOrder yet, so the query returns 404 NOT_FOUND. This is not a bug. When the cashier request included amount, the order is created immediately and this caveat does not apply.

  14. Trusting the cashier_token as a long-lived credential. The cashier token is a short-lived signed JWS scoped to the session. It rotates on session updates. Never persist it; always start fresh with POST /deposit/cashier.

Going live checklist

Before pointing production traffic at the gateway:

  • [ ] Callback URL is HTTPS, monitored, and returns 200 OK (or response body containing success) within 5 seconds.
  • [ ] Callback signature verification is implemented and unit-tested. See Authentication.
  • [ ] Your production NAT exit IPs are in the Cloudflare Access allowlist. See Sandbox for the Cloudflare edge source-IP model.
  • [ ] Idempotency strategy is decided: on POST /deposit retries you reuse the same merchant_order_no, and you have a code path to handle the duplicate response.
  • [ ] currency_code values are validated against the currencies your merchant account is enabled for. Sending an unsupported currency returns BUSINESS_ERROR.
  • [ ] Mock-channel sandbox tests pass for both Direct API and Cashier modes, covering the happy path and at least the EXPIRED branch.
  • [ ] Production API key and signing secret are stored in your secret manager. They are not in source, not in CI logs, and not in client-side bundles.
  • [ ] You have decided whether to expose the merchant order_no to your end users. It is safe to surface for customer-support workflows but is not a payer-facing identifier.

Cross-references

  • Authentication — MD5-over-sorted-params signing scheme used on every request and every callback.
  • Callbacks — Server-to-server status notifications, retry policy, response format.
  • Errors — Canonical error code reference. Cross-linked from the Pitfalls section.
  • API reference — Full schema definitions for every request and response field.
  • Sandbox — PRE environment, mock channel, Cloudflare edge IP allowlist model.
  • Quickstart — End-to-end example of creating a first deposit.
  • Withdrawals — The opposite direction; pays funds out from your merchant balance.