Slip verification
Two endpoints deal with slips:
| Endpoint | Use it when | Effect |
|---|---|---|
POST /v1/slips/verify | You want to know whether a slip is genuine and what it contains | Changes no payment |
POST /v1/payins/{id}/slip | The payer has paid but the pay-in was not matched yet, and you want to confirm it with the slip | If it matches, the pay-in becomes succeeded and a webhook is sent |
Verify a slip
Section titled “Verify a slip”POST /v1/slips/verifyTwo request forms:
- JSON: send the text of the slip’s QR code in
payload(max 2000 characters). Optionally addcheckDuplicate: trueto check whether the slip was used before. - multipart/form-data: send the slip image (JPG or PNG, max 5 MB) as the only part, named
file. Sign the hash of the file bytes; see multipart requests. (checkDuplicateis not available in this form.)
import { readFile } from 'node:fs/promises';import { api, signRequest } from './partner-api.mjs';
// 1) QR payloadconst { data: byPayload } = await api('POST', '/v1/slips/verify', { payload: qrText, checkDuplicate: true,});
// 2) Image: sign the file bytes, send only the `file` partconst bytes = await readFile('slip.jpg');const form = new FormData();form.append('file', new Blob([bytes], { type: 'image/jpeg' }), 'slip.jpg');const res = await fetch('https://api.example.com/v1/slips/verify', { method: 'POST', headers: signRequest('POST', '/v1/slips/verify', bytes), body: form,});const { data: byImage } = await res.json();// 1) QR payload$result = partner_api('POST', '/v1/slips/verify', ['payload' => $qrText, 'checkDuplicate' => true])['data'];
// 2) Image: sign the file bytes, send only the `file` part$path = '/v1/slips/verify';$ch = curl_init('https://api.example.com' . $path);curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => partner_sign('POST', $path, file_get_contents('slip.jpg')), CURLOPT_POSTFIELDS => ['file' => new CURLFile('slip.jpg', 'image/jpeg')], CURLOPT_RETURNTRANSFER => true,]);$result = json_decode(curl_exec($ch), true)['data'];curl_close($ch);import requestsfrom partner_api import api, sign_request
# 1) QR payloadresult = api("POST", "/v1/slips/verify", {"payload": qr_text, "checkDuplicate": True})["data"]
# 2) Image: sign the file bytes, send only the `file` partwith open("slip.jpg", "rb") as f: content = f.read()r = requests.post("https://api.example.com/v1/slips/verify", headers=sign_request("POST", "/v1/slips/verify", content), files={"file": ("slip.jpg", content, "image/jpeg")}, timeout=60)result = r.json()["data"]The result
Section titled “The result”A failed check still returns HTTP 200. Look at verified and failureCode.
{ "data": { "object": "slip_verification", "verified": true, "failureCode": null, "payload": "0041000600000101030040220...", "bankReference": "202601011234567890", "transferredAt": "2026-01-01T00:04:12.000Z", "amount": "150.37", "currency": "THB", "sender": { "bankCode": "SCB", "bankName": "Siam Commercial Bank", "accountNumber": "xxx-x-x4567-x", "accountName": "นาย สมชาย ส", "accountNameEn": "MR. SOMCHAI S" }, "receiver": { "bankCode": "KBANK", "bankName": "Kasikorn Bank", "accountNumber": "xxx-x-x5432-x", "accountName": "บจก. ตัวอย่าง", "accountNameEn": "EXAMPLE CO., LTD." } }, "requestId": "req_..."}failureCode | Meaning |
|---|---|
slip_not_found | No transfer matches the slip |
duplicate_slip | The slip was used before |
amount_mismatch | The amount does not match |
receiver_mismatch | The receiver does not match |
invalid_image | The image could not be read or the file is invalid |
invalid_payload | The payload is invalid |
verification_unavailable | Temporarily unavailable; try again later |
sender and receiver come from the bank. Some fields may be null or partly masked.
Testing with a test key
Section titled “Testing with a test key”With a test key (pk_test_...) the slip is not checked with the bank and no quota is used. Results are simulated from the payload:
payload starts with | Result |
|---|---|
TESTFAIL | verified: false, failureCode: "slip_not_found" |
TESTDUP | verified: false, failureCode: "duplicate_slip" |
| anything else (or an image) | verified: true, amount 100.00 THB, bankReference TEST-REF-0000000000 |
Errors
Section titled “Errors”| HTTP | code | Meaning |
|---|---|---|
| 400 | parameter_missing | Neither payload (JSON) nor file (multipart) was sent |
| 400 | file_invalid | No file, not a JPG/PNG, or larger than 5 MB |
| 400 | parameter_unknown | The multipart body has parts other than file |
| 422 | slip_verification_unavailable, slip_verification_failed | The verification service has a problem; try again later |
| 429 | quota_exceeded | Your slip verification quota is used up; contact the team |
Uploading a slip for a pay-in
Section titled “Uploading a slip for a pay-in”POST /v1/payins/{id}/slipUse it when the payer has transferred but the pay-in is still pending, for example when the payer sends you the slip. Send multipart/form-data with a single file part (JPG or PNG, max 5 MB) and sign the hash of the file bytes. See uploadSlip in the code samples.
{ "data": { "object": "slip_upload", "matched": true, "slipStatus": "matched", "payin": { "object": "payin", "id": "pin_3f2b1c0d9e8f4a7b8c6d5e4f3a2b1c0d", "status": "succeeded", "...": "..." } }, "requestId": "req_..."}matched: true: the slip matches and the pay-in is nowsucceeded. The usualpayin.succeededwebhook is sent.matched: false: not matched yet; checkslipStatusand the pay-in’s status.
| HTTP | code | Meaning |
|---|---|---|
| 400 | file_invalid | No file, not a JPG/PNG, or larger than 5 MB |
| 404 | resource_not_found | Unknown pay-in |
| 409 | payin_not_pending | The pay-in is no longer waiting for payment (succeeded, expired or canceled) |
| 409 | slip_duplicate | The slip was already used |
| 422 | slip_unreadable | The QR code on the slip could not be read; ask for a clearer image |
| 422 | slip_amount_mismatch | The slip amount differs from the pay-in’s amount |
| 422 | slip_receiver_mismatch | The receiving account differs from payTo |
| 422 | slip_verification_failed | The bank check failed |
| 422 | slip_verification_unavailable | Slip verification is temporarily off |