# ALL1PAY Merchant API v1 — TH

Revision: 2026-10-01

คู่มือนี้อ้างอิงแอปรุ่นที่เผยแพร่พร้อมกัน การแปลง CIMB, error Payin แบบแยก code และ Test Webhook ต้องใช้ API รุ่นที่รองรับ ระบบรุ่นเก่าอาจตอบ error แบบเดิมหรือยังไม่มีหน้าทดสอบ วันที่เอกสารไม่ใช่การเปลี่ยน URL จาก v1

Base URL: `https://service.all1pay.cloud/api`

สร้างจากข้อมูลชุดเดียวกับหน้า Docs และ API Docs ร้านค้า ตัวอย่างเป็นข้อมูลสมมติ ไม่ใช่หลักฐานว่ามีการชำระเงินจริง

## Contents

- [การยืนยันตัวตนและข้อจำกัดคำขอ](#request-rules)

- [รหัสธนาคาร Payin และชื่อฟิลด์](#banks)

- [HTTP error และผลธุรกรรม](#errors)

- [ตรวจปัญหาและปรับระบบเดิม](#troubleshooting)

- [ค่าธรรมเนียมและผลต่อกระเป๋า](#fees)

- [รายการซ้ำ timeout และการติดตามผล](#retries)

- [Webhook รายการเงินจริง](#webhooks)

- [Test Webhook — ทดสอบการเชื่อมต่อ](#webhook-test)

- [POST /v1/payin](#payin)

- [POST /v1/payin/:referenceId/verify-slip](#verify-slip)

- [POST /v1/payout](#payout)

- [GET /v1/transaction/:referenceId](#status)

- [GET /v1/transactions](#transactions)

- [GET /v1/balance](#balance)

- [POST /v1/transfer](#transfer)

<a id="request-rules"></a>
## การยืนยันตัวตนและข้อจำกัดคำขอ

Base URL: https://service.all1pay.cloud/api API สำหรับเชื่อมต่อร้านใช้ API credentials ไม่ใช่ JWT ของหน้าแผงควบคุม ทุกคำขอต้องส่ง x-api-key, x-secret-key, x-timestamp, x-signature เก็บ credentials ฝั่ง server ไม่ฝังใน browser/app ลูกค้า

x-timestamp เป็น Unix milliseconds 13 หลัก คลาดจาก server ไม่เกิน ±5 นาที ลายเซ็น hex HMAC-SHA256(secret, timestamp + METHODตัวใหญ่ + pathพร้อมqueryจริง + JSONแบบcompact) รวม /api ด้วย GET และ body ที่ไม่มีหรือ object ว่างใช้ข้อความว่าง เซ็น query ตามลำดับ/encoding ที่ส่งจริง

การเซ็นคำขอ API ฝั่ง server ใช้ JavaScript JSON.stringify(parsedBody) ไม่ใช้ช่องว่างหรือ escape จาก body ดิบ ต้องจัด JSON และลำดับ key ให้ตรง เก็บข้อความ Unicode เป็น UTF-8 (Python ใช้ ensure_ascii=False) และระวังรูปตัวเลข 1000.0 ต่างจาก 1000 ห้ามเรียง key ใหม่เอง ส่วนการรับ webhook ต้องตรวจจาก bytes ดิบเดิม ตัวอย่าง JavaScript และ Postman ใช้ JSON.stringify ให้แล้ว

ร้านต้อง ACTIVE และ IP ต้องผ่านรายการอนุญาต API หากตั้งไว้ credentials/ลายเซ็น/เวลาผิดได้ 401 ร้านปิดหรือ IP ไม่ผ่านได้ 403 รายการ IP ของ API แยกจากหน้าแผงควบคุม

จำกัดต่อร้าน นับรวม API key ทุกชุด: คำขอสร้างรายการ (payin, payout, verify-slip, transfer) 100 ครั้งที่ผ่านการตรวจต่อนาที และคำขออ่าน (สถานะ, รายการ, ยอดเงิน) อีก 200 ครั้งต่อนาทีแยกกัน การอ่านสถานะถี่จึงไม่ทำให้สร้าง payin หรือส่งสลิปไม่ได้ 429 ให้รอตามวินาทีใน Retry-After ใช้ backoff และ webhook แทน polling ถี่ JSON ปกติไม่เกิน 1 MiB; verify-slip ไม่เกิน 8 MiB เกินได้ 413

ส่งยอดเป็น JSON number และเลขบัญชี/รหัสธนาคารเป็น string ค่ายอดที่อ่านอาจเป็นข้อความทศนิยม ใช้วิธีคำนวณทศนิยมที่ไม่ตัดเศษ ตัวอย่างเป็นข้อมูลจำลองและเรทตัวอย่าง POST จาก Playground/Postman ทำงานจริงบน environment ที่เลือก

<a id="banks"></a>
## รหัสธนาคาร Payin และชื่อฟิลด์

Payin ใช้ bankName + bankNumber ระบุบัญชีต้นทางลูกค้า Payout ใช้ bankCode + accountNumber + accountName ระบุผู้รับ ต้อง map ชื่อไทยหรือ ID ภายในร้านเป็นรหัสธนาคาร ห้ามส่ง boolean, ID ตัวเลข หรือข้อความ false

Payin รองรับ alias ที่ยืนยันแล้วเฉพาะ CIMB → CIMBT ตัดช่องว่างหัวท้ายและปรับตัวพิมพ์ตามตาราง รวม Mizuho และ LH BANK alias อื่นที่ไม่รองรับถูกปฏิเสธ การละ/null ยังเป็นทางเลือกของ flow ที่ไม่ต้องใช้ ไม่ใช่วิธีเลี่ยงข้อมูลธนาคารที่ไม่ทราบ

ตารางคือรหัสที่ Payin รับ ไม่ได้รับประกันว่าทุกธนาคาร/flow เปิดอยู่ในทุก Provider ส่วน Payout ใช้สัญญารหัสตาม Provider ห้ามนำ mapping จ่ายเงินเฉพาะ Sandpay ไปใช้กับเจ้าอื่นหรือสมมติว่า alias Payin ใช้กับ Payout ด้วย

| bankName (Payin) | Bank / ธนาคาร |
| --- | --- |
| KBANK | กสิกรไทย |
| SCB | ไทยพาณิชย์ |
| BBL | กรุงเทพ |
| KTB | กรุงไทย |
| BAY | กรุงศรีอยุธยา |
| TTB | ทีเอ็มบีธนชาต |
| GSB | ออมสิน |
| BAAC | ธ.ก.ส. |
| GHB | อาคารสงเคราะห์ |
| KKP | เกียรตินาคินภัทร |
| TISCO | ทิสโก้ |
| CIMBT | ซีไอเอ็มบี ไทย |
| UOBT | ยูโอบี |
| LH BANK | แลนด์ แอนด์ เฮ้าส์ |
| TCRB | ไทยเครดิต |
| ISBT | อิสลามแห่งประเทศไทย |
| SME | เอสเอ็มอี พัฒนา |
| EXIM | เพื่อการส่งออกและนำเข้า |
| SCBT | สแตนดาร์ดชาร์เตอร์ด (ไทย) |
| CITI | ซิตี้แบงก์ |
| HSBC | เอชเอสบีซี |
| DB | ดอยซ์แบงก์ |
| BNPP | บีเอ็นพี พารีบาส์ |
| JPM | เจพีมอร์แกน เชส |
| BANA | แบงก์ออฟอเมริกา |
| BOC | แบงก์ออฟไชน่า (ไทย) |
| ICBC | ไอซีบีซี (ไทย) |
| MEGA | เมกะ สากลพาณิชย์ |
| OCBC | โอซีบีซี |
| RHB | อาร์เอชบี |
| IOB | อินเดียนโอเวอร์ซีส์ |
| SMBC | ซูมิโตโม มิตซุย |
| SMTB | ซูมิโตโม มิตซุย ทรัสต์ |
| Mizuho | มิซูโฮ |

<a id="errors"></a>
## HTTP error และผลธุรกรรม

แยกคำขอ HTTP สำเร็จ การสร้างรายการ และเงินสำเร็จ HTTP 201 ของ POST ไม่ใช่หลักฐานรับเงิน Payin/Payout ต้องดู status/errorMessage ส่วนสลิปดู success ห้ามเครดิตจากลิงก์ HTTP status หรือ providerData เพียงอย่างเดียว

400 อาจเป็นข้อมูลผิด การตั้งค่า รายการซ้ำ หรือเงินไม่พอ message อาจเป็นข้อความหรือรายการข้อความ เฉพาะ error Payin ที่ระบุด้านล่างรับประกัน code ห้ามใช้การตัดข้อความไทย/อังกฤษเป็นรหัสถาวรหรือสมมติว่าทุก error มี code

409 อาจหมายถึงหลักฐานต้องตรวจ ไม่ได้แปลว่าไม่มีการเปลี่ยนแปลงใด ๆ ถ้ามี referenceId/reviewRequired ให้เก็บและหยุดส่งซ้ำอัตโนมัติ 5xx/timeout ก็ยังสรุปไม่ได้ ส่วนข้อมูลผิด 400 ต้องแก้ข้อมูล ไม่ใช่ยิงเดิมซ้ำ

| HTTP 400 code | Extra field | Meaning / การจัดการ |
| --- | --- | --- |
| INVALID_BANK_CODE | supportedBankCodes | รหัสธนาคาร Payin ที่ส่งไม่ถูกต้อง ดู supportedBankCodes การปฏิเสธนี้ยังไม่สร้างธุรกรรม |
| PAYIN_AMOUNT_BELOW_MINIMUM | minimumAmount | ต่ำกว่าขั้นต่ำเส้นทางที่ใช้ได้ อ่าน minimumAmount ไม่กำหนดค่าตายตัวทุกร้าน |
| PAYIN_PROVIDER_UNAVAILABLE | — | ไม่มีเส้นทางรับเงินที่เปิดและใช้ได้สำหรับร้าน ให้ support ตรวจการผูก/สถานะ |
| PAYIN_FLOW_NOT_SUPPORTED | — | เส้นทางที่ใช้ได้ไม่รองรับ flow ที่ขอ |
| PAYIN_AMOUNT_ABOVE_MAXIMUM | maximumAmount | เกินยอดสูงสุดของเส้นทางที่ใช้ได้ |
| PAYIN_AMOUNT_OUT_OF_RANGE | — | อยู่นอกช่วงยอดที่เส้นทางรองรับ เช่น ช่องว่างระหว่างสองช่วง ให้ support ตรวจ |


```json
{
  "statusCode": 400,
  "code": "PAYIN_AMOUNT_BELOW_MINIMUM",
  "message": "ยอดเงินต่ำกว่าขั้นต่ำ: กรุณาสร้างรายการรับเงินตั้งแต่ 300 บาท",
  "minimumAmount": 300
}
```


HTTP 409 (payout review):


```json
{
  "message": "พักรายการเพื่อตรวจสอบข้อมูล Provider ยังไม่ลงเงินหรือคืนเงิน กรุณาติดต่อทีมดูแล (SETTLEMENT_REVIEW_REQUIRED)",
  "referenceId": "TXN-EXAMPLE-PAYOUT",
  "reviewRequired": true
}
```


<a id="troubleshooting"></a>
## ตรวจปัญหาและปรับระบบเดิม

ถ้าบางลูกค้าใช้ได้แต่บางรายไม่ได้ ให้เทียบ JSON ที่ส่งและผลตอบของแต่ละรายการ ชื่อธนาคารที่แสดงเหมือนกันอาจ map เป็นรหัสคนละค่า กรุงศรีใช้ BAY; CIMB ไทยใช้ CIMBT แก้ mapping และหยุดที่ฝั่งร้านหากหาไม่เจอ ห้ามส่ง false หรือแทนเป็นธนาคารอื่น อายุบัญชีเพียงอย่างเดียวไม่บอกสาเหตุ

Send Test Order ในเครื่องมือ Provider ของแอดมินเรียกต้นทางโดยตรง ไม่ได้ทดสอบขั้นตอนเลือกเส้นทาง/ค่าธรรมเนียม/กระเป๋าของร้านครบ การดูเส้นทางใหม่เป็นการอ่านเงื่อนไข ไม่ได้สร้างออเดอร์หรือลิงก์จ่ายเงินจริง ต้องตรวจผ่าน API ของร้านที่ผูก Provider และส่ง flowType ชัดเจน

แยกข้อความให้ลูกค้า: ธนาคารผิดให้แก้ธนาคารต้นทาง; ต่ำกว่าขั้นต่ำให้แสดง minimumAmount; ไม่มี Provider ให้ทีมงานตรวจการผูกร้าน สถานะ Provider/บัญชีและความพร้อม อย่าแปลง error ทุกชนิดเป็นไม่มีผู้ให้บริการ หากได้ paymentUrl แล้วลูกค้ายังไม่ชำระ การหมดอายุภายหลังต่างจากการสร้างไม่ผ่าน

ก่อนเปลี่ยนระบบเดิม ตรวจชื่อ/ชนิดฟิลด์ รหัสธนาคาร slip/qr เลข order ที่คงเดิม การคิดทศนิยม การตรวจลายเซ็น และ webhook ซ้ำ/มาช้า ทดสอบธนาคารผิด/ต่ำกว่าขั้นต่ำ/เส้นทางปิดในสภาพแวดล้อมแยกที่ตกลงกัน webhook.test ต้องไม่เปลี่ยนยอดเงิน ยืนยันรุ่น API ที่ deploy กับ ALL1PAY ก่อนพึ่ง error code ใหม่

แจ้ง support ด้วยรหัสร้าน referenceId ถ้าได้รับ orderId เวลาส่งพร้อมเขตเวลา ยอด flowType รหัสธนาคาร HTTP status และ code/message ปิดเลขบัญชีและข้อมูลส่วนตัว ห้ามส่ง API secret, webhook secret หรือ header ยืนยันตัวตนเต็ม ตรวจรายการเดิมก่อนสร้างการชำระซ้ำ

<a id="fees"></a>
## ค่าธรรมเนียมและผลต่อกระเป๋า

เลือกเรทร้านแยกรับและจ่าย หากตั้งเปอร์เซ็นต์ร้านไว้รวม 0 ใช้เปอร์เซ็นต์และค่าคงที่ของร้าน มิฉะนั้นใช้เรทการผูก Provider ที่เลือก เลือกหนึ่งแหล่งไม่บวกสองเรท และต้นทุน Provider ไม่ใช่ค่าธรรมเนียมร้านอีกชั้น

ค่าธรรมเนียม = ยอด × เปอร์เซ็นต์ / 100 + ค่าคงที่ ปัดสองตำแหน่ง Payin สำเร็จเครดิตยอดหักค่าธรรมเนียม Payout พักยอดบวกค่าธรรมเนียม ผู้รับได้เงินต้น amount รายการบันทึกยอด/ค่าธรรมเนียม/เรทของตัวเอง การเปลี่ยนเรทปัจจุบันไม่แก้รายการย้อนหลัง

ตัวอย่างเท่านั้น: 1,000 บาท เรทรับ 1.5% ไม่มีค่าคงที่ ค่าธรรมเนียม 15 สุทธิ 985 ส่วนจ่าย 1,000 เรท 0.2% ค่าธรรมเนียม 2 totalDebit 1,002 อ่านค่าธรรมเนียมรายการจริง ไม่ตั้งเรทตัวอย่างตายตัว

<a id="retries"></a>
## รายการซ้ำ timeout และการติดตามผล

Payout ต้องมี orderId ส่วน Payin แนะนำอย่างยิ่ง เลขไม่ซ้ำภายในร้านทั้งสองประเภท สร้างซ้ำได้ HTTP 400 ไม่ได้ตอบผลเดิมกลับมา และไม่รองรับ header Idempotency-Key การเซ็นเวลาใหม่ไม่ได้เปลี่ยนรายการธุรกิจเดิมให้เป็นรายการใหม่

บันทึก orderId/คำขอก่อนส่งและ referenceId ทันทีที่ได้รับ หาก timeout ให้ตรวจ reference เดิมหรือหา merchantOrderId ในรายการแบบแบ่งหน้า หากไม่ชัดให้ติดต่อ support ห้ามออก orderId Payout ใหม่เพื่อข้ามรายการซ้ำ/error/ผลที่ไม่ทราบ

PENDING/PROCESSING ไม่ใช่ล้มเหลวสุดท้าย และ EXPIRED ไม่ยืนยันว่าไม่มีคนโอน Payin EXPIRED อาจเป็น COMPLETED ภายหลังเมื่อยืนยันหลักฐาน ต้องรองรับ callback ช้า/ซ้ำและการแก้สถานะ เวลาข้อความใหม่ไม่ใช่เงินรายการใหม่

กันส่งซ้ำด้วย webhook-id และกันบันทึกเงินซ้ำระดับรายการด้วย referenceId/type ในฐานข้อมูล การส่งใหม่ด้วยมืออาจมี webhook ID ใหม่แต่เป็นรายการเดิม ถ้าลำดับสถานะขัดกันให้ตรวจ status/support ห้ามย้อนเครดิตที่ยืนยันแล้วจากข้อความเก่า

| status | Meaning / ความหมาย |
| --- | --- |
| PENDING | สร้างแล้วหรือรอดำเนินการ/ตรวจสอบ Payout อาจพักเงินแล้ว |
| PROCESSING | กำลังดำเนินการ ยังไม่ยืนยันเงินสำเร็จ |
| COMPLETED | ALL1PAY บันทึกสำเร็จแล้ว |
| FAILED | บันทึกล้มเหลว อย่าสับสนกับ timeout/รอตรวจ ตรวจรายการเดิมก่อนชำระใหม่ |
| EXPIRED | หมดอายุ ไม่ใช่หลักฐานว่าไม่มีการโอน อาจกู้ Payin ด้วยหลักฐานที่ตรวจแล้ว |
| REFUNDED | รายการสำเร็จเดิมถูกคืนเงินผ่านกระบวนการที่ได้รับอนุญาต |

<a id="webhooks"></a>
## Webhook รายการเงินจริง

ตั้ง callback URL และ Webhook Secret ที่ Webhooks → Settings (รุ่นเก่า: Settings → Integration) callbackUrl เฉพาะรายการมีลำดับก่อนค่าเริ่มต้น นี่คือสัญญา callback ของ ALL1PAY ไม่ใช่ payload order.paid/payout.approved ของต้นทาง

รับ POST JSON มี referenceId, merchantOrderId, status, amount, currency, type ฟิลด์เวลาแตกต่างกัน: การเปลี่ยนสถานะอาจมี completedAt ส่งซ้ำด้วยมืออาจมี resent=true, resentAt ห้ามใช้เวลาตัดสินเงินสำเร็จ ไม่รับประกัน callback ทุกสถานะกลางหรือการปฏิเสธสร้างรายการ ต้องเก็บ/ตรวจผล create ด้วย

Header มี Content-Type: application/json และ x-webhook-id ส่วน x-webhook-signature เป็น hex HMAC-SHA256(JSON body ดิบ, webhook secret ร้าน) ส่งเมื่อมี secret ตั้งไว้เท่านั้น ต้องตั้ง secret ก่อนใช้งานรับลายเซ็น และปลายทางต้องปฏิเสธลายเซ็นหาย/ผิด

ตรวจลายเซ็นจาก bytes เดิมก่อนแปลง/จัดรูป JSON แล้วบันทึกหรือเข้าคิวอย่างคงทนและตอบ 2xx โดยเร็ว ส่งจริงมี HTTP timeout 10 วินาที ไม่ตาม redirect หากล้มเหลว/ไม่ใช่ 2xx ส่งซ้ำหลัง 1, 5, 15, 60 นาที รวมไม่เกิน 5 ครั้ง เวลาจริงอาจช้าตาม outage/load URL ที่ไม่ปลอดภัยหยุดส่งซ้ำ

ส่ง webhook ไม่สำเร็จไม่ได้แปลว่าเงินล้มเหลว ตรวจ Webhooks → Delivery Logs (เมนูเก่า Webhook Logs) และ status API ห้ามสร้าง Payout ใหม่เพียงเพราะปลายทาง callback ล้มเหลว


```json
{
  "referenceId": "TXN-EXAMPLE-PAYIN",
  "merchantOrderId": "PAYIN-EXAMPLE-001",
  "status": "COMPLETED",
  "amount": 1000,
  "currency": "THB",
  "type": "PAYIN",
  "completedAt": "2026-09-16T07:00:00.000Z"
}
```


Verify x-webhook-signature using your **webhook secret** and the original raw body:


```javascript
const crypto = require('node:crypto');

// rawBody is the original request body, before JSON parsing.
function verifyWebhook(rawBody, signature, webhookSecret) {
  if (!/^[a-f0-9]{64}$/i.test(signature || '')) return false;
  const expected = crypto.createHmac('sha256', webhookSecret).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}
```


<a id="webhook-test"></a>
## Test Webhook — ทดสอบการเชื่อมต่อ

ใช้ได้เมื่อเปิดรุ่น Webhooks hub และเป็นเจ้าของร้านที่ active ส่ง webhook.test, test=true และ ID ขึ้นต้น TEST- ไม่มีจำนวนเงิน/สถานะชำระ ตรวจลายเซ็น JSON ดิบแล้วตอบรับ ห้ามเพิ่ม/หักยอดกระเป๋าจากเหตุการณ์นี้

ระบุ URL ทดสอบ HTTPS สาธารณะ port 443 โดยไม่เปลี่ยนค่าเริ่มต้นที่บันทึก รอทดสอบ 5 วินาที ไม่มี retry อัตโนมัติ บันทึกใน Test history แยก HTTP 2xx แปลว่าปลายทางรับคำขอ ไม่ยืนยันว่าโค้ดตรวจลายเซ็น/บันทึกเงินถูกต้อง

จำกัดรายการทดสอบใหม่ 1 ครั้งต่อ 30 วินาที และ 10 ครั้งต่อชั่วโมงต่อร้าน รวมทั้งความจุส่วนกลาง หากผลหายให้ตรวจ Test history/ผลรายการเดิมก่อนส่งใหม่


```json
{
  "event": "webhook.test",
  "test": true,
  "id": "TEST-example",
  "createdAt": "2026-09-16T07:00:00.000Z",
  "message": "ALL1PAY connection test. No payment is associated with this event."
}
```


<a id="payin"></a>
## สร้างรายการรับเงิน

`POST /v1/payin`

JSON body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| amount | number | Yes | จำนวนเงินอย่างน้อย 1 บาท เส้นทางที่ใช้มีขั้นต่ำหรือสูงสุดเพิ่มเติมได้ ส่งยอด THB ไม่เกินสองตำแหน่งทศนิยม |
| currency | string | No | ค่าเริ่มต้น THB ใช้ THB กับเส้นทางรับ–จ่ายเงินไทยในคู่มือนี้ การมีฟิลด์นี้ไม่ได้ยืนยันว่ารองรับทุกสกุลเงิน |
| orderId | string | No | แนะนำให้ระบุและไม่ซ้ำภายในร้านทั้ง Payin และ Payout หากละจะไม่มีการกันสร้างซ้ำด้วยเลขร้านนี้ |
| providerAccountId | string | No | ละเพื่อให้เลือกเส้นทางตามการตั้งค่าร้าน หากระบุต้องเป็นบัญชีที่ผูกและเปิดใช้กับร้าน รองรับยอด/ช่องทาง การทดสอบ Provider โดยตรงไม่ได้ยืนยันว่าร้านใช้เส้นทางนั้นได้ |
| callbackUrl | string | No | ปลายทาง callback เฉพาะรายการ หากไม่ได้ระบุใช้ค่าเริ่มต้นร้าน ต้องเป็น HTTPS สาธารณะ ไม่มีรหัสผ่านฝังใน URL หรือ redirect ไม่รับที่อยู่ภายใน/สงวน |
| customerName | string | No | ชื่อลูกค้า ไม่ใช่หลักฐานยืนยันเจ้าของบัญชี |
| customerEmail | string | No | อีเมลลูกค้า |
| bankName | string | No | รหัสธนาคารต้นทางของลูกค้า ส่งคู่ bankNumber สำหรับช่องทาง slip ที่ผูกบัญชีผู้โอน ดูตารางธนาคาร Payin; CIMB แปลงเป็น CIMBT ไม่ใช่ธนาคารปลายทางและไม่ใช่ฟิลด์ bank_name |
| bankNumber | string | No | เลขบัญชีต้นทางลูกค้า ส่งตัวเลขเป็นข้อความและเก็บศูนย์นำหน้า การตรวจความยาว/เจ้าของบัญชียังขึ้นกับ Provider |
| customerPhone | string | No | เบอร์ลูกค้าหากเส้นทางนั้นต้องใช้ |
| flowType | slip \| qr | No | ค่าเริ่มต้น slip ควรส่ง slip หรือ qr ชัดเจน metadata.channel ไม่ใช้เลือก flow กลาง และร้านต้องเปิดช่องทางนั้นไว้ |
| metadata | object | No | ข้อมูลเสริมแบบ object ไม่เกิน 16 KiB ลึก 8 ชั้นและ 512 ค่า รายการธุรกรรมไม่ส่ง metadata อิสระทั้งหมดกลับมา ฟิลด์ระดับบนที่ไม่รองรับถูกปฏิเสธ |

Request example:


```json
{
  "amount": 1000,
  "currency": "THB",
  "orderId": "PAYIN-EXAMPLE-001",
  "flowType": "slip",
  "bankName": "SCB",
  "bankNumber": "0012345678"
}
```


Response example — HTTP 201:


```json
{
  "referenceId": "TXN-EXAMPLE-PAYIN",
  "status": "PROCESSING",
  "paymentUrl": "https://payment.example.com/order/example",
  "amount": 1000,
  "currency": "THB",
  "providerRefId": "ORD-EXAMPLE",
  "providerData": {}
}
```


Response fields: `referenceId`, `status`, `paymentUrl?`, `amount`, `currency`, `providerRefId?`, `providerData`, `errorMessage?`

- HTTP 201 อาจมี status FAILED และ errorMessage เมื่อต้นทางปฏิเสธ PROCESSING พร้อมลิงก์หมายถึงสร้างรายการ ไม่ใช่ชำระแล้ว ตรวจ body ก่อนแสดงหน้าจ่ายเงิน

- QR: ส่ง flowType=qr เมื่อร้านเปิดใช้ ละข้อมูลธนาคารได้ในเส้นทางที่ไม่ต้องใช้ ส่วน slip ให้แสดงหน้าชำระ/บัญชีปลายทางที่ได้รับและส่งสลิปจริง ห้ามสร้างข้อมูลปลายทางเอง

- paymentUrl/providerRefId อาจไม่มี providerData มีรูปแบบตาม Provider ไม่รับประกัน qrCode, qrCodeUrl หรือ expiresAt ระดับบน อย่าสมมติว่าทุกเจ้าส่ง destination_bank หรือ QR base64 ในชื่อเดียวกัน

- หากคำตอบหายหรือได้ 5xx อาจสร้างรายการแล้ว ต้องตรวจรายการเดิมก่อนสร้างใหม่ Endpoint นี้สร้างรายการจริง ไม่ใช่ sandbox

<a id="verify-slip"></a>
## ตรวจสลิป

`POST /v1/payin/:referenceId/verify-slip`

JSON body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| qrString | string | No | ไม่บังคับ ส่งคู่กับ imageBase64 ได้: ข้อความ QR ที่อ่านจากสลิปโอนเงิน ไม่ใช่ QR สำหรับจ่ายเงิน บาง Provider ตรวจจากข้อความนี้อย่างเดียวไม่ได้ |
| imageBase64 | string | No | ส่งทุกครั้ง: รูปสลิปจริงแบบ base64 เป็น JPEG หรือ PNG (data:image/jpeg;base64,... หรือ base64 ล้วน) บาง Provider ตรวจได้จากรูปเท่านั้นและจะปฏิเสธถ้าส่งแค่ qrString HTTP body รวมไม่เกิน 8 MiB base64 ทำให้ขนาดโตขึ้นราว 1/3 รูปจึงควรไม่เกินราว 6 MB |

Request example:


```json
{
  "imageBase64": "data:image/jpeg;base64,REPLACE_WITH_SLIP_IMAGE_BASE64",
  "qrString": "REPLACE_WITH_ACTUAL_SLIP_QR"
}
```


Response example — HTTP 201:


```json
{
  "success": true,
  "status": "paid",
  "applied": true,
  "rawResponse": {}
}
```


Response fields: `success`, `status?`, `applied?`, `alreadyCompleted?`, `pending?`, `code?`, `message?`, `errorMessage?`, `rawResponse?`

- ส่งรูปสลิป (imageBase64) ทุกครั้ง จะส่ง qrString มาด้วยก็ได้ API รับค่าใดค่าหนึ่งก็ได้ แต่ Provider ที่ตรวจจากรูปเท่านั้นจะตอบ success=false ถ้าส่งแค่ qrString ต้องเป็นรายการของร้านและมีเลขต้นทาง Provider ต้องรองรับการตรวจสลิป รายการ Payin PROCESSING หรือ EXPIRED อาจกู้คืนได้

- เติมเครดิตครั้งเดียวต่อ 1 ออเดอร์ เฉพาะเมื่อได้คำตอบ applied=true หรือสถานะธุรกรรม/callback เป็น COMPLETED ส่งสลิปเข้าออเดอร์ที่ COMPLETED แล้วจะได้ success=false, code=ALREADY_PAID, alreadyCompleted=true คือออเดอร์จ่ายแล้ว ห้ามเติมซ้ำ สลิปที่เคยใช้จ่ายออเดอร์อื่นแล้วจะได้ success=false, code=SLIP_ALREADY_USED ถ้า Provider ยังไม่ตอบผล จะได้ success=false, status=pending, pending=true ให้ลูกค้ารอ แล้วติดตามสถานะธุรกรรม

- ต้นทางปฏิเสธอาจตอบ HTTP 201 พร้อม success=false และ errorMessage หลักฐานที่ต้องตรวจอาจได้ HTTP 409 ห้ามเครดิตจาก rawResponse หรือคำว่า paid ของต้นทาง ให้ยืนยันสถานะธุรกรรม ALL1PAY

<a id="payout"></a>
## สร้างรายการจ่ายเงิน

`POST /v1/payout`

JSON body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| amount | number | Yes | จำนวนเงินอย่างน้อย 1 บาท เส้นทางที่ใช้มีขั้นต่ำหรือสูงสุดเพิ่มเติมได้ ส่งยอด THB ไม่เกินสองตำแหน่งทศนิยม |
| currency | string | No | ค่าเริ่มต้น THB ใช้ THB กับเส้นทางรับ–จ่ายเงินไทยในคู่มือนี้ การมีฟิลด์นี้ไม่ได้ยืนยันว่ารองรับทุกสกุลเงิน |
| bankCode | string | Yes | รหัสธนาคารผู้รับที่ Provider จ่ายเงินรองรับ การแปลง CIMB ของ Payin ไม่ได้ใช้กับ Payout ต้องยืนยัน mapping ของเส้นทางจ่ายเงิน |
| accountNumber | string | Yes | เลขบัญชีผู้รับ เก็บศูนย์นำหน้า |
| accountName | string | Yes | ชื่อเจ้าของบัญชีผู้รับ |
| orderId | non-empty string | Yes | เลขรายการร้านที่ต้องไม่ซ้ำทั้ง Payin และ Payout เก็บเลขรายการเดิมไว้ขณะตรวจผลที่ยังไม่ชัดเจน |
| providerAccountId | string | No | ละเพื่อให้เลือกเส้นทางตามการตั้งค่าร้าน หากระบุต้องเป็นบัญชีที่ผูกและเปิดใช้กับร้าน รองรับยอด/ช่องทาง การทดสอบ Provider โดยตรงไม่ได้ยืนยันว่าร้านใช้เส้นทางนั้นได้ |
| callbackUrl | string | No | ปลายทาง callback เฉพาะรายการ หากไม่ได้ระบุใช้ค่าเริ่มต้นร้าน ต้องเป็น HTTPS สาธารณะ ไม่มีรหัสผ่านฝังใน URL หรือ redirect ไม่รับที่อยู่ภายใน/สงวน |
| metadata | object | No | ข้อมูลเสริมแบบ object ไม่เกิน 16 KiB ลึก 8 ชั้นและ 512 ค่า รายการธุรกรรมไม่ส่ง metadata อิสระทั้งหมดกลับมา ฟิลด์ระดับบนที่ไม่รองรับถูกปฏิเสธ |

Request example:


```json
{
  "amount": 1000,
  "currency": "THB",
  "bankCode": "KBANK",
  "accountNumber": "0012345678",
  "accountName": "Example Recipient",
  "orderId": "PAYOUT-EXAMPLE-001"
}
```


Response example — HTTP 201:


```json
{
  "referenceId": "TXN-EXAMPLE-PAYOUT",
  "status": "PROCESSING",
  "reviewRequired": false,
  "amount": 1000,
  "currency": "THB",
  "feeAmount": 2,
  "totalDebit": 1002,
  "providerRefId": "PAYOUT-EXAMPLE",
  "providerData": {}
}
```


Response fields: `referenceId`, `status`, `reviewRequired`, `amount`, `currency`, `feeAmount`, `totalDebit`, `providerRefId?`, `providerData`, `errorMessage?`

- amount คือเงินต้นที่ขอจ่ายผู้รับ feeAmount คือค่าธรรมเนียมร้าน และ totalDebit=amount+feeAmount พักเงินรวมนี้ระหว่างดำเนินการ ตัวอย่าง 0.2% ไม่ใช่เรทของทุกร้าน

- timeout, ไม่มีเลขต้นทาง หรือ reviewRequired=true ไม่ใช่หลักฐานว่าล้มเหลวหรือคืนเงินแล้ว ความผิดพลาดที่ยังสรุปไม่ได้อาจตอบ HTTP 409 พร้อม referenceId และ reviewRequired=true หยุดสร้างซ้ำด้วย orderId ใหม่แล้วติดตามรายการเดิม

- HTTP 201 อาจเป็น FAILED หลังปฏิเสธชัดเจน หรือ PENDING/PROCESSING ที่รอตรวจ COMPLETED จึงเป็นสถานะสำเร็จที่ยืนยันแล้ว ไม่มีการส่ง Payout รายการนี้ไป Provider อื่นอัตโนมัติ

<a id="status"></a>
## ตรวจสถานะรายการ

`GET /v1/transaction/:referenceId`

Query parameters:

Response example — HTTP 200:


```json
{
  "referenceId": "TXN-EXAMPLE-PAYIN",
  "orderId": "PAYIN-EXAMPLE-001",
  "type": "PAYIN",
  "status": "COMPLETED",
  "amount": "1000",
  "currency": "THB",
  "fee": "15",
  "createdAt": "2026-09-16T07:00:00.000Z",
  "completedAt": "2026-09-16T07:01:00.000Z"
}
```


Response fields: `referenceId`, `orderId`, `type`, `status`, `amount`, `currency`, `fee`, `createdAt`, `completedAt`

- อ่านสถานะที่ ALL1PAY บันทึก ไม่ได้ยิงถาม Provider สด referenceId เป็นเลข ALL1PAY ไม่ใช่ orderId หรือ providerRefId เลขที่ไม่มีหรือของร้านอื่นได้ HTTP 400 Transaction not found

- Endpoint นี้ใช้ orderId และ fee แต่รายการรวม/webhook ใช้ merchantOrderId ยอด amount และ fee ในคำตอบนี้เป็นข้อความทศนิยม อย่าสมมติว่าทุก endpoint ส่งยอดเป็น JSON number และ endpoint นี้ยังไม่ส่ง reviewRequired

<a id="transactions"></a>
## รายการธุรกรรม

`GET /v1/transactions`

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| page | query integer | No | ค่าเริ่มต้น 1 ไม่รับ offset เกิน 10,000 แถว ใช้ nextCursor เมื่ออ่านประวัติลึก |
| limit | query integer 1–100 | No | ค่าเริ่มต้น 20 |
| type | query PAYIN \| PAYOUT | No | กรองประเภทรายการ |
| status | query status | No | กรองสถานะตามตารางสถานะ |
| cursor | query string | No | คัดลอก meta.nextCursor ตามเดิม คงตัวกรอง type/status และละ page หากเป็น null หรือ data ว่างให้หยุด |

Response example — HTTP 200:


```json
{
  "data": [
    {
      "id": "EXAMPLE-ID",
      "referenceId": "TXN-EXAMPLE-PAYIN",
      "merchantOrderId": "PAYIN-EXAMPLE-001",
      "type": "PAYIN",
      "status": "PROCESSING",
      "amount": "1000",
      "currency": "THB",
      "merchantFeeAmount": "15",
      "customerName": null,
      "customerEmail": null,
      "bankCode": null,
      "accountNumber": null,
      "accountName": null,
      "paymentUrl": "https://payment.example.com/order/example",
      "providerRefId": "ORD-EXAMPLE",
      "expiredAt": null,
      "completedAt": null,
      "failedReason": null,
      "createdAt": "2026-09-16T07:00:00.000Z",
      "updatedAt": "2026-09-16T07:00:00.000Z",
      "reviewRequired": false,
      "metadata": {
        "bankName": "SCB",
        "bankNumber": "0012345678"
      }
    }
  ],
  "meta": {
    "total": 1,
    "page": 1,
    "limit": 20,
    "totalPages": 1,
    "nextCursor": null
  }
}
```


Response fields: `data`, `meta.total`, `meta.page`, `meta.limit`, `meta.totalPages`, `meta.nextCursor`

- เรียงใหม่ก่อนตาม createdAt/id ตัวกรองและ cursor จำกัดเฉพาะร้าน Public v1 ไม่รองรับ dateFrom, dateTo, search หรือค้นด้วย orderId อย่าอ้างคำอธิบาย Postman เก่าที่ระบุว่าทำได้

- ใช้ pagination แบบจำกัดเพื่อหารายการที่คำตอบหายด้วย merchantOrderId แล้วเก็บ referenceId หากยังไม่แน่ใจให้ติดต่อ support ที่นี่มี reviewRequired ส่วน metadata ส่งเฉพาะ bankName/bankNumber ไม่ใช่ข้อมูลเสริมทั้งหมด ยอดที่เป็น Decimal ส่งเป็นข้อความ

<a id="balance"></a>
## ยอดกระเป๋าร้าน

`GET /v1/balance`

Query parameters:

Response example — HTTP 200:


```json
{
  "merchantId": "EXAMPLE-MERCHANT",
  "currency": "THB",
  "payinBalance": 50000,
  "payoutBalance": 12000,
  "holdBalance": 3000,
  "totalBalance": 62000,
  "availablePayout": 11000,
  "maxSinglePayout": 11000,
  "updatedAt": "2026-09-16T07:00:00.000Z"
}
```


Response fields: `merchantId`, `currency`, `payinBalance`, `payoutBalance`, `holdBalance`, `totalBalance`, `availablePayout`, `maxSinglePayout`, `updatedAt`

- payinBalance ได้เครดิตสุทธิหลัง Payin สำเร็จ payoutBalance คือกระเป๋าจ่ายเงิน holdBalance คือยอดพัก totalBalance=payinBalance+payoutBalance ไม่รวม holdBalance

- availablePayout หักยอด freeze จ่ายเงินที่ป้องกันไว้และไม่ติดลบ จึงอาจต่ำกว่า payoutBalance ยอดที่อ่านไม่ได้รับประกันว่า Payout ถัดไปจะผ่าน เพราะยังมีการจัดสรรต่อ Provider และรายการพร้อมกัน

- maxSinglePayout คือยอดสูงสุดที่ Payout หนึ่งรายการถอนได้ตอนนี้ เพราะ Payout แต่ละรายการจ่ายจากบัญชีผู้ให้บริการบัญชีเดียว เมื่อยอดแยกอยู่หลายบัญชี ค่านี้จึงอาจต่ำกว่า availablePayout ยอดที่มากกว่านี้ให้แบ่งเป็นหลายรายการ

<a id="transfer"></a>
## โยกเงินรับไปกระเป๋าจ่าย

`POST /v1/transfer`

JSON body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| amount | number | Yes | จำนวนเงินย้ายจากกระเป๋ารับเงินไปกระเป๋าจ่ายเงิน อย่างน้อย 1 บาท ต้องมียอดรับเงินที่ใช้ได้เพียงพอ |

Request example:


```json
{
  "amount": 1000
}
```


Response example — HTTP 201:


```json
{
  "payinBefore": 50000,
  "payinAfter": 49000,
  "payoutBefore": 12000,
  "payoutAfter": 13000,
  "transferAmount": 1000
}
```


Response fields: `payinBefore`, `payinAfter`, `payoutBefore`, `payoutAfter`, `transferAmount`

- โยกเงินในกระเป๋าร้านทันที ไม่ได้โอนไปบัญชีธนาคารภายนอก ยอดรับไม่พอได้ HTTP 400 ขั้นต่ำ Provider ไม่ใช้กับการโยกภายในนี้

- Endpoint นี้ไม่มี orderId หรือ idempotency key การส่งซ้ำอาจโยกซ้ำ หาก timeout ให้ตรวจ Wallet Activity และติดต่อ support ก่อน POST อีกครั้ง ห้าม retry อัตโนมัติ

## Signed request examples

ตัวอย่าง flow qr ต้องเปิดใช้กับร้านก่อน เก็บ orderId เดิมของงานไว้ ห้ามเปลี่ยนเป็นเลขใหม่เพื่อ retry เมื่อยังไม่ทราบผลลัพธ์ และห้ามถือว่า HTTP 201 คือเงินสำเร็จ

### JavaScript


```javascript
const crypto = require('node:crypto');

async function createPayin() {
const apiKey = process.env.ALL1PAY_API_KEY;
const secret = process.env.ALL1PAY_API_SECRET;
const url = new URL('https://service.all1pay.cloud/api/v1/payin');
const timestamp = Date.now().toString();
const body = JSON.stringify({
  "amount": 1000,
  "currency": "THB",
  "orderId": "ORDER-12345",
  "flowType": "qr",
  "customerName": "Example Customer"
});
const message = timestamp + 'POST' + url.pathname + url.search + body;
const signature = crypto.createHmac('sha256', secret).update(message).digest('hex');
const response = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey,
    'x-secret-key': secret, 'x-timestamp': timestamp, 'x-signature': signature },
  body,
});
const result = await response.json();
// Persist referenceId + orderId even when HTTP/business processing fails.
console.log({ httpStatus: response.status, ...result });
if (!response.ok) throw new Error('ALL1PAY HTTP ' + response.status + ': ' + JSON.stringify(result));
if (result.status === 'FAILED' || result.reviewRequired) throw new Error('Payin not accepted: ' + JSON.stringify(result));
// PROCESSING / paymentUrl means an order exists, not that money was received.
// Credit only after verified COMPLETED evidence; reconcile timeouts before retrying.
return result;
}

createPayin().catch(error => { console.error(error); process.exitCode = 1; });
```


### Python


```python
import hashlib, hmac, json, os, time, requests
from urllib.parse import urlsplit
url = 'https://service.all1pay.cloud/api/v1/payin'
secret = os.environ['ALL1PAY_API_SECRET']
timestamp = str(int(time.time() * 1000))
body = json.dumps({"amount":1000,"currency":"THB","orderId":"ORDER-12345","flowType":"qr","customerName":"Example Customer"}, separators=(',', ':'), ensure_ascii=False)
parts = urlsplit(url)
path = parts.path + ('?' + parts.query if parts.query else '')
signature = hmac.new(secret.encode(), (timestamp + 'POST' + path + body).encode(), hashlib.sha256).hexdigest()
response = requests.post(url, data=body.encode('utf-8'), timeout=30, headers={
    'Content-Type': 'application/json', 'x-api-key': os.environ['ALL1PAY_API_KEY'],
    'x-secret-key': secret, 'x-timestamp': timestamp, 'x-signature': signature,
})
result = response.json()
print(response.status_code, result)  # Persist referenceId + orderId, including errors.
response.raise_for_status()
if result.get('status') == 'FAILED' or result.get('reviewRequired'):
    raise RuntimeError('Payin not accepted: ' + json.dumps(result))
# PROCESSING/paymentUrl is not paid. Reconcile timeouts before retrying.
```


### PHP


```php
<?php
$url = 'https://service.all1pay.cloud/api/v1/payin';
$secret = getenv('ALL1PAY_API_SECRET');
$timestamp = (string) round(microtime(true) * 1000);
$body = '{"amount":1000,"currency":"THB","orderId":"ORDER-12345","flowType":"qr","customerName":"Example Customer"}';
$path = parse_url($url, PHP_URL_PATH);
$query = parse_url($url, PHP_URL_QUERY);
if ($query) $path .= '?' . $query;
$signature = hash_hmac('sha256', $timestamp . 'POST' . $path . $body, $secret);
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
  CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [
    'Content-Type: application/json', 'x-api-key: ' . getenv('ALL1PAY_API_KEY'),
    'x-secret-key: ' . $secret, 'x-timestamp: ' . $timestamp, 'x-signature: ' . $signature,
  ]]);
$response = curl_exec($ch);
if ($response === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $response; // Persist referenceId + orderId, including error responses.
if ($status < 200 || $status >= 300) throw new RuntimeException('ALL1PAY HTTP ' . $status . ': ' . $response);
$result = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
if (($result['status'] ?? '') === 'FAILED' || !empty($result['reviewRequired'])) throw new RuntimeException('Payin not accepted: ' . $response);
// PROCESSING/paymentUrl is not paid. Reconcile timeouts before retrying.
```


### cURL


```curl
# Set ALL1PAY_API_KEY and ALL1PAY_API_SECRET in your environment.
TIMESTAMP=$(node -p 'Date.now()')
BODY='{"amount":1000,"currency":"THB","orderId":"ORDER-12345","flowType":"qr","customerName":"Example Customer"}'
SIGNATURE=$(printf '%s' "${TIMESTAMP}POST/api/v1/payin${BODY}" | openssl dgst -sha256 -hmac "$ALL1PAY_API_SECRET" | awk '{print $NF}')
curl --fail-with-body 'https://service.all1pay.cloud/api/v1/payin' \
  -H 'Content-Type: application/json' \
  -H "x-api-key: $ALL1PAY_API_KEY" -H "x-secret-key: $ALL1PAY_API_SECRET" \
  -H "x-timestamp: $TIMESTAMP" -H "x-signature: $SIGNATURE" \
  --data-raw "$BODY"
```


## Postman

Import `ALL1PAY-Merchant-API.postman_collection.json`. Set `base_url`, `api_key`, `secret_key` locally. Credentials ship blank. Keep secrets out of shared exports.

POST is disabled by default. Set `allow_live_requests=true` only for an intentional request against your selected environment. Set a stable `payin_order_id` or `payout_order_id` for one business operation; the collection never creates new IDs automatically. Do not run all POSTs or repeat a collection runner blindly. Status, list and balance calls count toward the merchant read limit, separate from POST requests.

The pre-request script canonicalizes JSON once and signs the exact resolved path including /api and query. Keep query values URL-encoded. Timestamp and signature are regenerated per request, not the orderId. Requires Postman scripting (CryptoJS); no actual request is sent by generating this file.

## Changelog

2026-10-10: verify-slip: ส่งสลิปเข้าออเดอร์ที่ COMPLETED แล้ว ตอนนี้ได้ success=false พร้อม code=ALREADY_PAID (เดิมได้ success=true) สลิปที่เคยใช้จ่ายออเดอร์อื่นแล้วได้ code=SLIP_ALREADY_USED และถ้า Provider ยังไม่ตอบผล จะได้ status=pending, pending=true แทนข้อความผิดพลาด

2026-10-01: verify-slip ให้ส่งรูปสลิป (imageBase64) ทุกครั้ง qrString ส่งคู่ได้ เพราะบาง Provider ตรวจจาก qrString อย่างเดียวไม่ได้

2026-10-01: แยกโควตาต่อนาทีเป็นคำขอสร้างรายการ (payin, payout, verify-slip, transfer) 100 ครั้ง กับคำขออ่าน (สถานะ, รายการ, ยอดเงิน) 200 ครั้ง และ 429 มี Retry-After

2026-09-16: รวมฟิลด์/HTTP status รหัสธนาคาร ความหมายค่าธรรมเนียม/พักเงิน การติดตามผล pagination และ webhook การแปลงธนาคาร/error ใหม่ต้องใช้ backend รุ่นเดียวกัน
