Pay-ins
A pay-in collects money from one payer. You create it, the platform returns where to pay (a QR code or a destination account), the payer transfers, and the platform matches the transfer and notifies you with a webhook.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/payins | Create a pay-in |
GET | /v1/payins/{id} | Retrieve a pay-in, including payTo (?include=paymentImage adds the image) |
GET | /v1/payins | Search pay-ins |
POST | /v1/payins/{id}/cancel | Cancel a pending or expired pay-in |
POST | /v1/payins/{id}/slip | Upload the payer’s slip (see Slip verification) |
Create a pay-in
Section titled “Create a pay-in”POST /v1/payins| Field | Required | Description |
|---|---|---|
payer | one of payer / payerId | The payer’s bank account: bankCode, accountNumber, accountName, optional accountNameEn. See Inline payer |
payerId | one of payer / payerId | An existing payer’s payer_... id (Payers) |
amount | yes | Decimal string such as "150.00", between your account’s payinMinimum and payinMaximum |
currency | yes | THB, USD, USDT or LAK |
method | no | auto (default), promptpay_qr, bank_transfer, crypto, lao_qr |
reference | no | Your order number, max 128 characters. Returned in responses and webhooks, and searchable |
webhookUrl | yes | http(s) URL that receives webhooks, max 2048 characters |
returnUrl | no | Where the hosted payment page sends the payer afterwards |
{ "payer": { "bankCode": "SCB", "accountNumber": "1234567890", "accountName": "สมชาย สบายดี" }, "amount": "150.00", "currency": "THB", "reference": "order-10001", "webhookUrl": "https://shop.example.com/webhooks/payments", "returnUrl": "https://shop.example.com/orders/10001"}If you already store payer ids, send "payerId": "payer_7c0a4b8e2f3d4a1b9c8d7e6f5a4b3c2d" instead of payer.
Query parameter: include=paymentImage adds the ready-made image to the response (see below).
Good to know:
- The payer must transfer the
amountof the response. The platform may add a few satang to match the transfer, soamount(e.g.150.37) can differ fromrequestedAmount(150.00). Showamountto the payer. - The channel is chosen for you. For Thai baht,
promptpay_qrandbank_transferrequests are routed automatically. Themethodof the response shows the channel actually used. - One open pay-in per payer. While a payer has a
pendingpay-in, creating another returns409 pending_payin_exists. Wait for it to finish or cancel it first. - For
crypto,payTocontainswalletAddressandnetwork. - Send an
Idempotency-Keyheader so a retry after a timeout cannot create a second pay-in. See Retry a create safely.
Inline payer
Section titled “Inline payer”Send exactly one of payerId and payer. Sending both returns 400 parameters_exclusive, sending neither returns 400 parameter_missing.
- Existing account: reused. If one of your payers already has the same
bankCodeandaccountNumber, the pay-in uses that payer. Its stored name is not changed, because the name is used to match the incoming transfer. If the name you sent is different (case and extra spaces are ignored), the pay-in is still created and the response has awarningsentrypayer_name_differs. Change the name withPATCH /v1/payers/{id}if needed. - New account: created. Otherwise a new
activepayer is created with exactly the same checks asPOST /v1/payers(field validation, blocked accounts). Errors point at the nested field, e.g.param: "payer.accountNumber"; an unknown bank is422 bank_not_foundwithparam: "payer.bankCode". Simultaneous requests for the same account create only one payer. - The new payer is kept even if the pay-in itself is then rejected (for example
amount_below_minimum); a retry reuses it. - An inactive or blocked payer gives the same errors as with
payerId(payer_inactive,payer_blocked), withparam: "payer". - The response contains
payerIdandpayer. Store the id if you want to usepayerIdnext time.
{ "data": { "object": "payin", "id": "pin_...", "payerId": "payer_7c0a4b8e2f3d4a1b9c8d7e6f5a4b3c2d", "...": "..." }, "warnings": [ { "code": "payer_name_differs", "message": "An existing payer with this bank account was used. Its stored account name differs from the one sent and was kept; it is used for payment matching.", "param": "payer.accountName" } ], "requestId": "req_..."}warnings is only present when there is a warning.
QR code and payment image
Section titled “QR code and payment image”payTo.qrPayload is the text inside the payment QR code. Render it with any QR library to show your own QR code:
method | qrPayload |
|---|---|
promptpay_qr | EMVCo/PromptPay payload; it already contains amount |
lao_qr | Lao QR payload |
bank_transfer, crypto | null (show the account or wallet from payTo) |
paymentImage, a ready-made PNG of the QR code or transfer card, is not returned by default. Add ?include=paymentImage to POST /v1/payins or GET /v1/payins/{id} to get it. include takes a comma-separated list and unknown values are ignored. The query string is part of the signature, so sign /v1/payins?include=paymentImage.
The pay-in object
Section titled “The pay-in object”Create and retrieve (GET /v1/payins/{id}) also return payTo, and paymentImage when requested with ?include=paymentImage. Search results and webhooks include neither.
{ "object": "payin", "id": "pin_3f2b1c0d9e8f4a7b8c6d5e4f3a2b1c0d", "reference": "order-10001", "payerId": "payer_7c0a4b8e2f3d4a1b9c8d7e6f5a4b3c2d", "payer": { "object": "payer", "id": "payer_7c0a4b8e2f3d4a1b9c8d7e6f5a4b3c2d", "bankCode": "SCB", "bankName": "Siam Commercial Bank", "accountNumber": "1234567890", "accountName": "สมชาย สบายดี", "accountNameEn": "Somchai Sabuydee", "status": "active", "referralCode": null, "createdAt": "2026-01-01T00:00:00.000Z" }, "requestedAmount": "150.00", "amount": "150.37", "fee": "2.25", "currency": "THB", "method": "promptpay_qr", "status": "pending", "failureReason": null, "webhookUrl": "https://shop.example.com/webhooks/payments", "returnUrl": "https://shop.example.com/orders/10001", "checkoutUrl": "https://pay.example.com/3f2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d", "payerAccount": { "bankCode": "SCB", "accountNumber": "1234567890", "accountName": "สมชาย สบายดี" }, "payTo": { "bankCode": "KBANK", "bankName": "Kasikorn Bank", "accountNumber": "0987654321", "accountName": "Example Co., Ltd.", "promptpayId": "0812345678", "qrPayload": "00020101021229370016A000000677010111011300668123456785802TH53037645406150.3763046572" }, "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z"}| Field | Description |
|---|---|
requestedAmount | The amount you asked for |
amount | The amount the payer must transfer (including any added satang) |
fee | Total fee of the pay-in |
method | The channel actually used |
failureReason | Short text when the pay-in did not succeed (expired, canceled…), otherwise null |
checkoutUrl | Hosted payment page. Can be null if that service is not enabled; then show payTo (or a QR code from payTo.qrPayload) yourself |
payerAccount | The payer’s account used for this pay-in |
payTo | The account or wallet the payer must transfer to, and qrPayload |
payTo.qrPayload | Text of the payment QR code for promptpay_qr / lao_qr, otherwise null |
paymentImage | Only with ?include=paymentImage: PNG data URI (QR code or transfer card), ready for an <img> |
Whether you credit your user with requestedAmount or amount depends on your contract; ask the team if unsure.
Statuses
Section titled “Statuses”| Status | Meaning | Final |
|---|---|---|
pending | Created, waiting for the payer’s transfer | no |
processing | Being processed | no |
succeeded | The transfer was matched (bank statement, slip or manual confirmation) | yes |
expired | The payer did not pay in time | yes (can still be canceled) |
canceled | Canceled by you or by the system | yes |
rejected | Rejected | yes |
failed | Could not be completed; see failureReason | yes |
Update your orders from webhooks, and use GET to double-check when in doubt. See Webhooks.
Search pay-ins
Section titled “Search pay-ins”GET /v1/payins?status=pending,processing&createdFrom=2026-01-01T00:00:00Z&limit=50| Parameter | Description |
|---|---|
status | One status, or several separated by , |
reference | Exact match on reference |
payerId | Only this payer’s pay-ins |
createdFrom, createdTo | Creation time range, ISO-8601, inclusive |
limit, cursor | Pagination, see the Overview |
Cancel a pay-in
Section titled “Cancel a pay-in”POST /v1/payins/{id}/cancelNo body (sign the hash of the empty string). Only pending or expired pay-ins can be canceled. Returns the pay-in with status canceled. Cancels made through the API do not send a webhook; the response is your confirmation.
Common errors
Section titled “Common errors”| HTTP | code | Meaning and what to do |
|---|---|---|
| 400 | parameter_missing, parameter_invalid | A field is missing or malformed (also neither payerId nor payer); see error.param |
| 400 | parameters_exclusive | Both payerId and payer were sent; send one |
| 400 | unsupported_currency, unsupported_method | Currency or channel not supported |
| 400 | invalid_webhook_url, invalid_return_url | The URL is not http(s) or is malformed |
| 403 | payins_disabled | Pay-ins are disabled for your account; contact support |
| 404 | payer_not_found | payerId does not exist or is not yours |
| 422 | bank_not_found | payer.bankCode is not a code from GET /v1/banks |
| 404 | resource_not_found | Unknown pay-in id |
| 409 | pending_payin_exists | The payer already has an open pay-in |
| 409 | payin_not_cancelable | Cannot be canceled in its current status |
| 422 | amount_below_minimum, amount_above_maximum | Outside your account limits; the message states the limit |
| 422 | amount_below_fee | The amount is lower than the fee |
| 422 | payer_inactive, payer_blocked | The payer is inactive or blocked |
| 422 | no_collection_account_available | No collection account is free right now; try again later |
| 422 | not_available_now, maintenance | Outside service hours or under maintenance; try again later |
Every code is listed in Errors.