How to use P2P

The short version: call the same APIs, let the platform pick the rail, then wait for the webhook

P2P is not a new API. It is a relief rail the platform uses to take load off the main bank account: you call the same endpoints and write the same code, and the platform decides which rail each transaction takes.

  • Deposits normally land in the main bank account. When that account is unavailable the platform routes the deposit through P2P, and if no withdrawal is there to match it, the money goes straight to a bank account within the same request — you still get one PAYMENT_PAID.
  • A withdrawal with no match, or one that is only partly filled, is topped up by the platform itself within 15 minutes. Nothing is left hanging.
  • Only two things are actually different: the withdrawal has to go to PromptPay, and you close the order from the webhook — never when the create call succeeds.

1. The flow in four steps

  1. 1

    Get the account ready

    Register the webhook URL and top up THB_P2P credit — fees and central-account transfers are deducted from it.

  2. 2

    Deposit: create a slip session

    Call Create Slip Payment and send the depositor to the redirect_url. They transfer within 10 minutes and attach the slip on that page; the system checks it, then PAYMENT_PAID arrives.

  3. 3

    Withdrawal: PromptPay only

    Create the withdrawal with receiver_bank "PromptPay". The system fills it leg by leg from deposits, and tops up whatever is still short within 15 minutes.

  4. 4

    Settle from the webhook

    Close the order only when the terminal event arrives, and poll the stretches that stay silent.

2. Create a deposit or a withdrawal

Both sides start with one API call. A deposit creates a slip session and hands the depositor a hosted page; a withdrawal is the normal THB withdrawal endpoint locked to PromptPay. You never check for a match first — the platform decides inside that one request, and everything after that arrives through the webhook.

Deposit: Create Slip Payment

The depositor transfers and attaches the slip on our page — matched or not, you get the same result back

Call /v1/p2p/public/session/create with role "DEPOSITOR", the amount, and the bank account the customer will transfer from. The response carries a redirect_url — send the depositor there. That page shows the destination account, takes the slip, and checks it; you just wait for the result.

POSTCreate Slip Payment
Endpoint URL
https://testnet.trustsig.xyz/v1/p2p/public/session/create
REQUESTX-Idempotency-Key is required — reuse the same key when you retry
POST /v1/p2p/public/session/create
Content-Type: application/json
X-Idempotency-Key: <UUID>

{
    "role": "DEPOSITOR",
    "ttl_seconds": 1200,
    "profile": {
        "depositor": {
            "sender_bank": "SCB",
            "sender_account": "1234567890"
        }
    },
    "amount": 2500,
    "order_id": "ACME-D-2026-000123",
    "order_user_reference": "USER134",
    "return_url": "https://merchant.example.com/deposit/done"
}
  • Redirect the depositor to redirect_url as soon as you get it. The transfer, the slip upload, and the slip check all happen on that page — there is no separate slip API to call on the P2P rail.
  • ttl_seconds is how long the payment link lives. The 10-minute transfer window is a different clock — it starts once the depositor is matched and sees the destination account.
  • The depositor has to transfer from the exact sender_bank / sender_account you sent — a slip from another account will not clear.
  • No withdrawal to match means the platform switches to a direct bank transfer inside the same request. deposit_type comes back as CLASSIC, but payment.id and PAYMENT_PAID are the same as ever — do not tie your logic to the rail.
  • Do not credit the customer when the session is created or when the page returns to return_url. PAYMENT_PAID from the webhook is what actually closes the deposit; a reused slip comes back as PAYMENT_FAILED.
  • A rejected slip does not kill the order — the depositor can attach another one from the same page until the window closes.

Withdrawal: PromptPay only

Same endpoint as a THB withdrawal, but the destination has to be PromptPay

P2P withdrawals go out over PromptPay and nothing else. Use the same createRequest/fiat endpoint as a normal THB withdrawal, but set receiver_bank to "PromptPay" and put the phone number or national ID in withdrawal_address.

POSTCreate Withdrawal (THB)
Endpoint URL
https://testnet.trustsig.xyz/v1/withdrawal/createRequest/fiat
REQUESTreceiver_bank must be "PromptPay" for the request to run as P2P
POST /v1/withdrawal/createRequest/fiat
Content-Type: application/json

{
    "amount": 10000,
    "currency": "THB",
    "receiver_bank": "PromptPay",
    "withdrawal_address": "0812345678",
    "receiver_name": "MR. John Snow",
    "order_id": "ACME-W-2026-000456",
    "order_user_reference": "USER900"
}
  • Bank accounts are not accepted on the P2P rail — a request with any other receiver_bank will not go through as P2P.
  • PromptPay is required because the depositor has to scan and pay within the window, and the destination can be matched more precisely.
  • Keep the returned id — GET /v1/withdrawal/detail/:id is the only way to read the legs back.
  • No match, or still short after 15 minutes, and the platform transfers the remainder itself. The withdrawal still ends as WITHDRAWAL_COMPLETED; that top-up comes out of your THB_P2P credit.

3. Set up the webhook

Webhooks are how you find out anything finished. Register a URL, turn on HMAC, and verify the signature against JSON.stringify(body.data) — not the raw body. Answer 2xx within 10 seconds and do the real work asynchronously.

Two traps worth knowing up front: with no webhook URL registered you get no events and no error either, and changing the URL blocks new withdrawals for 24 hours.