Skip to content

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.

MethodPathPurpose
POST/v1/payinsCreate a pay-in
GET/v1/payins/{id}Retrieve a pay-in, including payTo (?include=paymentImage adds the image)
GET/v1/payinsSearch pay-ins
POST/v1/payins/{id}/cancelCancel a pending or expired pay-in
POST/v1/payins/{id}/slipUpload the payer’s slip (see Slip verification)
POST /v1/payins
FieldRequiredDescription
payerone of payer / payerIdThe payer’s bank account: bankCode, accountNumber, accountName, optional accountNameEn. See Inline payer
payerIdone of payer / payerIdAn existing payer’s payer_... id (Payers)
amountyesDecimal string such as "150.00", between your account’s payinMinimum and payinMaximum
currencyyesTHB, USD, USDT or LAK
methodnoauto (default), promptpay_qr, bank_transfer, crypto, lao_qr
referencenoYour order number, max 128 characters. Returned in responses and webhooks, and searchable
webhookUrlyeshttp(s) URL that receives webhooks, max 2048 characters
returnUrlnoWhere 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 amount of the response. The platform may add a few satang to match the transfer, so amount (e.g. 150.37) can differ from requestedAmount (150.00). Show amount to the payer.
  • The channel is chosen for you. For Thai baht, promptpay_qr and bank_transfer requests are routed automatically. The method of the response shows the channel actually used.
  • One open pay-in per payer. While a payer has a pending pay-in, creating another returns 409 pending_payin_exists. Wait for it to finish or cancel it first.
  • For crypto, payTo contains walletAddress and network.
  • Send an Idempotency-Key header so a retry after a timeout cannot create a second pay-in. See Retry a create safely.

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 bankCode and accountNumber, 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 a warnings entry payer_name_differs. Change the name with PATCH /v1/payers/{id} if needed.
  • New account: created. Otherwise a new active payer is created with exactly the same checks as POST /v1/payers (field validation, blocked accounts). Errors point at the nested field, e.g. param: "payer.accountNumber"; an unknown bank is 422 bank_not_found with param: "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), with param: "payer".
  • The response contains payerId and payer. Store the id if you want to use payerId next 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.

payTo.qrPayload is the text inside the payment QR code. Render it with any QR library to show your own QR code:

methodqrPayload
promptpay_qrEMVCo/PromptPay payload; it already contains amount
lao_qrLao QR payload
bank_transfer, cryptonull (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.

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"
}
FieldDescription
requestedAmountThe amount you asked for
amountThe amount the payer must transfer (including any added satang)
feeTotal fee of the pay-in
methodThe channel actually used
failureReasonShort text when the pay-in did not succeed (expired, canceled…), otherwise null
checkoutUrlHosted payment page. Can be null if that service is not enabled; then show payTo (or a QR code from payTo.qrPayload) yourself
payerAccountThe payer’s account used for this pay-in
payToThe account or wallet the payer must transfer to, and qrPayload
payTo.qrPayloadText of the payment QR code for promptpay_qr / lao_qr, otherwise null
paymentImageOnly 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.

StatusMeaningFinal
pendingCreated, waiting for the payer’s transferno
processingBeing processedno
succeededThe transfer was matched (bank statement, slip or manual confirmation)yes
expiredThe payer did not pay in timeyes (can still be canceled)
canceledCanceled by you or by the systemyes
rejectedRejectedyes
failedCould not be completed; see failureReasonyes

Update your orders from webhooks, and use GET to double-check when in doubt. See Webhooks.

GET /v1/payins?status=pending,processing&createdFrom=2026-01-01T00:00:00Z&limit=50
ParameterDescription
statusOne status, or several separated by ,
referenceExact match on reference
payerIdOnly this payer’s pay-ins
createdFrom, createdToCreation time range, ISO-8601, inclusive
limit, cursorPagination, see the Overview
POST /v1/payins/{id}/cancel

No 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.

HTTPcodeMeaning and what to do
400parameter_missing, parameter_invalidA field is missing or malformed (also neither payerId nor payer); see error.param
400parameters_exclusiveBoth payerId and payer were sent; send one
400unsupported_currency, unsupported_methodCurrency or channel not supported
400invalid_webhook_url, invalid_return_urlThe URL is not http(s) or is malformed
403payins_disabledPay-ins are disabled for your account; contact support
404payer_not_foundpayerId does not exist or is not yours
422bank_not_foundpayer.bankCode is not a code from GET /v1/banks
404resource_not_foundUnknown pay-in id
409pending_payin_existsThe payer already has an open pay-in
409payin_not_cancelableCannot be canceled in its current status
422amount_below_minimum, amount_above_maximumOutside your account limits; the message states the limit
422amount_below_feeThe amount is lower than the fee
422payer_inactive, payer_blockedThe payer is inactive or blocked
422no_collection_account_availableNo collection account is free right now; try again later
422not_available_now, maintenanceOutside service hours or under maintenance; try again later

Every code is listed in Errors.