Skip to content

Quickstart

By the end of this guide you will have signed and sent your first sandbox deposit request, read the payment instructions returned by the API, and queried the order back from the system. The whole loop should take under ten minutes.

This chapter is a worked example. For depth on any topic it touches, follow the cross-links into the rest of the documentation.

What you need

Before you start, make sure you have all five of the following. If any are missing, contact your account manager before continuing — none of the steps below will work without them.

  • A sandbox merchant_id and api_key, provisioned by your account manager.
  • Your source IP added to the sandbox Cloudflare Access allowlist. This is handled by your account manager. For background on the Cloudflare Access source-IP layer, see Sandbox.
  • The signing helper for your language: sign.sh (bash), sign.py (Python), sign.php (PHP), or sign.ts (Node.js). All four are byte-for-byte equivalent — pick the one closest to your stack.
  • Any HTTP client: curl, Postman, HTTPie, or your language's HTTP library.
  • A clock that is within ±5 minutes of NTP. The server rejects requests outside this window.

This guide uses Python in the snippets below, but every step works identically in the other three languages.

Step 1: Sign a deposit request

Every merchant API call carries three headers: X-API-Key, X-Timestamp, and X-Signature. The signature is a lowercase-hex MD5 over the sorted request parameters with your api_key appended. The signing reference implementations live in docs-site/shared/code-samples/.

Start by deciding what payload you want to send. The deposit-create endpoint accepts the following fields, defined by DepositOrderCreate in backend/app/schemas/order.py:

FieldTypeRequiredNotes
merchant_order_nostringyesYour own order identifier. 1–64 chars. Must be unique per merchant.
amountdecimalyesAmount to deposit. Must be > 0. Send as a string to preserve precision.
currency_codestringyesISO 4217 three-letter code, e.g. THB, VND, IDR.
callback_urlstringyesHTTPS URL the gateway will POST asynchronous state updates to.
user_idstringnoYour end user's identifier, if you want to attach one. Up to 64 chars.
extra_datastringnoOpaque metadata, echoed back to you on callbacks.

For this example, use the following payload:

json
{
  "merchant_order_no": "QUICKSTART-0001",
  "amount": "100.00",
  "currency_code": "THB",
  "callback_url": "https://your-domain.example/callbacks/deposit"
}

Now compute the signature. The Python reference helper looks like this:

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())

To use it, pass each request parameter — including the current Unix timestamp — as a k=v&k=v string, then append your API key:

bash
API_KEY="pre_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TIMESTAMP=$(date +%s)

PARAMS="amount=100.00&callback_url=https://your-domain.example/callbacks/deposit&currency_code=THB&merchant_order_no=QUICKSTART-0001&timestamp=${TIMESTAMP}"

SIGNATURE=$(python sign.py --api-key "$API_KEY" --params "$PARAMS")
echo "$SIGNATURE"

The same call shape works with sign.sh, sign.php, and sign.ts — only the interpreter changes. The four implementations are pinned together by a CI parity test, so any of them produces an identical digest from the same inputs.

For the underlying algorithm, the worked digest example, and the four full implementations, see Authentication & Signing.

Step 2: Send the request

Once you have a fresh signature, POST the JSON body to the deposit-create endpoint. The path is /api/v1/merchant/orders/deposit — the /orders segment comes from the merchant router prefix and is easy to miss.

bash
API_KEY="pre_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE_URL="https://pre-api.open-pay.co"
TIMESTAMP=$(date +%s)

PARAMS="amount=100.00&callback_url=https://your-domain.example/callbacks/deposit&currency_code=THB&merchant_order_no=QUICKSTART-0001&timestamp=${TIMESTAMP}"
SIGNATURE=$(python sign.py --api-key "$API_KEY" --params "$PARAMS")

curl -X POST "${BASE_URL}/api/v1/merchant/orders/deposit" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Signature: ${SIGNATURE}" \
  -d '{
    "merchant_order_no": "QUICKSTART-0001",
    "amount": "100.00",
    "currency_code": "THB",
    "callback_url": "https://your-domain.example/callbacks/deposit"
  }'

A few placeholders to substitute before you run this:

  • API_KEY — the sandbox api_key your account manager gave you. Sandbox keys are typically prefixed pre_.
  • callback_url — a publicly reachable HTTPS endpoint you control. A request-bin or an ngrok tunnel into your dev machine works for the first run.
  • merchant_order_no — unique per merchant. If you repeat the same value, the second call will fail with a duplicate-order error.

The JSON body holds the same parameters that went into the signature, minus timestamp (which travels in the X-Timestamp header). Note that the signature does not care about the JSON key order in transit — only that the keys were sorted alphabetically when you computed the digest.

Step 3: Read the response

A successful request returns HTTP 201 Created and the standard envelope. The data block is shaped by DepositPaymentInfo from backend/app/schemas/order.py. There are two flavors of data, depending on whether the order was routed to a payment channel or to a bank account on our books:

json
{
  "success": true,
  "code": "SUCCESS",
  "message": "OK",
  "data": {
    "order_no": "DEP20260520000001",
    "requested_amount": "100.00",
    "amount": "100.00",
    "currency_code": "THB",
    "bank_code": "KBANK",
    "bank_name": "ธนาคารกสิกรไทย (Kasikorn Bank)",
    "bank_branch": null,
    "account_no": "1234567890",
    "account_holder": "Mock Payment Co., Ltd.",
    "remark": "DEP20260520000001",
    "expire_at": "2026-05-20T08:15:00Z",
    "payment_url": null,
    "payment_info": null
  }
}

The fields you will use most often:

  • order_no — the gateway-side identifier for this deposit. Save it. You will use it to query state and to correlate the asynchronous callback.
  • amount — the actual amount the payer should transfer. This may differ from requested_amount if the channel applies amount-floating to make orders distinguishable; show amount to the payer.
  • expire_at — the cutoff after which the order will no longer accept payment.
  • bank_code, bank_name, account_no, account_holder, remark — the payment instructions to display to the payer for an account-routed order.
  • payment_url, payment_info — populated instead for channel-routed orders. Redirect the payer to payment_url, or render payment_info yourself.

For the complete envelope contract, including the catalog of error codes you should branch on, see Errors. For every field on every endpoint, see the API Reference.

Step 4: Query the order

To confirm the order landed in the system, query it back by order_no. The query endpoint is GET-only, so there is no body to sign — the signature is computed over the timestamp alone.

bash
ORDER_NO="DEP20260520000001"
API_KEY="pre_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TIMESTAMP=$(date +%s)

PARAMS="timestamp=${TIMESTAMP}"
SIGNATURE=$(python sign.py --api-key "$API_KEY" --params "$PARAMS")

curl -X GET "https://pre-api.open-pay.co/api/v1/merchant/query/deposit/${ORDER_NO}" \
  -H "X-API-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Signature: ${SIGNATURE}"

A fresh order will come back with status: "pending" and no matched_at or completed_at yet:

json
{
  "success": true,
  "code": "SUCCESS",
  "message": "OK",
  "data": {
    "id": "01J...",
    "order_no": "DEP20260520000001",
    "merchant_order_no": "QUICKSTART-0001",
    "merchant_id": "01J...",
    "amount": "100.00",
    "currency_code": "THB",
    "status": "pending",
    "expire_at": "2026-05-20T08:15:00Z",
    "matched_at": null,
    "completed_at": null,
    "callback_url": "https://your-domain.example/callbacks/deposit",
    "callback_status": null,
    "callback_count": 0,
    "created_at": "2026-05-20T08:00:00Z",
    "updated_at": "2026-05-20T08:00:00Z"
  }
}

That status: "pending" is your success criterion. The order is in the gateway, it has accepted your signature, and it is waiting for the payer to transfer. From here, state transitions arrive asynchronously through the callback_url you supplied.

Next steps

You have proven the integration end-to-end. The chapters below go deeper on each piece:

  • Authentication — the full signing reference, the 5-minute timestamp window, the worked digest example, and all four language helpers side by side.
  • Deposits — the full deposit guide, including cashier-mode integration and the deposit state machine (PENDINGMATCHEDCOMPLETED and its failure paths).
  • Callbacks — how the gateway delivers asynchronous state updates, the callback signing scheme, and your retry semantics.
  • Sandbox — the full sandbox-versus-production breakdown, the Cloudflare Access source-IP layer, mock-channel behavior, and the common sandbox pitfalls.
  • Errors — the envelope contract, the catalog of code values you should branch on, and HTTP-status retry guidance.