Slip API
ตรวจสลิปโอนเงินธนาคารลาวและไทยด้วยภาพเดียว: อ่านข้อมูลสลิป ตรวจเลขอ้างอิงซ้ำ บัญชีผู้รับ ยอดเงิน เวลาโอน และร่องรอยการแต่งรูป แล้วตอบผลเป็น pass review หรือ reject
เริ่มต้น
Base URL: https://api.kuadslip.com/v1
ทุกคำขอส่ง API key ใน header Authorization: Bearer slp_live_... (หรือ X-API-Key). Key หนึ่งผูกกับหนึ่งโปรเจกต์ — สลิป เลขอ้างอิงซ้ำ ค่าตั้ง และ webhook แยกกันต่อโปรเจกต์
เรียกจากเซิร์ฟเวอร์ของคุณเท่านั้น อย่าใส่ API key ในแอปมือถือหรือหน้าเว็บ — API นี้ไม่เปิด CORS
ตรวจสลิป
POST/v1/slips/verify
ส่งเป็น multipart/form-data (แนะนำ) หรือ JSON ที่มี image_base64. ภาพ JPEG / PNG / WEBP / GIF ไม่เกิน 4MB (ทั้ง request ไม่เกิน ~4.4MB)
| ฟิลด์ | ชนิด | คำอธิบาย |
|---|---|---|
image | file | ภาพสลิป (multipart) — หรือ image_base64 ใน JSON |
expected_amount | number | ยอดของออเดอร์ ต้องตรงกับยอดในสลิป ไม่ส่ง = ไม่ตรวจยอด |
expected_currency | LAK | THB | USD | ค่าเริ่มต้น = currency ของโปรเจกต์ (LAK) |
accounts | string[] หรือ "a,b" | บัญชีผู้รับของร้าน (เลขเต็ม หรือ 4 ตัวท้าย) ทับค่าตั้งโปรเจกต์ |
max_age_min | number | สลิปต้องโอนไม่เกินกี่นาทีก่อนเรียก API (ค่าเริ่มต้น 30, 0 = ไม่จำกัด) |
order_id | string | รหัสออเดอร์ของคุณ ใช้ค้นหา และทำให้การส่งซ้ำ (retry) ปลอดภัย |
curl https://api.kuadslip.com/v1/slips/verify \ -H "Authorization: Bearer $SLIP_API_KEY" \ -F image=@slip.jpg \ -F expected_amount=150000 \ -F order_id=CF-10293
// Node 20+
const form = new FormData();
form.append('image', new Blob([await fs.readFile('slip.jpg')]), 'slip.jpg');
form.append('expected_amount', '150000');
form.append('order_id', 'CF-10293');
const r = await fetch('https://api.kuadslip.com/v1/slips/verify', {
method: 'POST', headers: { Authorization: `Bearer ${process.env.SLIP_API_KEY}` }, body: form,
});
const result = await r.json();
if (result.status === 'accepted') { /* ยืนยันออเดอร์ */ }
ส่งซ้ำด้วย order_id เดิมและสลิปเดิม (เช่น timeout แล้ว retry) จะได้ผลเดิมกลับมาพร้อม "replayed": true ไม่ถูกนับว่าเลขซ้ำ
ผลตอบกลับ
{
"id": "6c3a…",
"object": "slip_verification",
"status": "accepted", // accepted | pending_review | rejected — ใช้ค่านี้ตัดสิน
"verdict": "pass", // pass | review | reject — ผลตรวจอัตโนมัติ
"reasons": [], // [{ code, message, message_th }]
"order_id": "CF-10293",
"expected": { "amount": 150000, "currency": "LAK" },
"slip": {
"bank": "BCEL One", "ref": "00123456789", "txn_at": "2026-10-31T20:50:00+07:00",
"amount": 150000, "currency": "LAK", "fee": null,
"sender": { "name": "SOMSAK", "account": "0101XXXXXXX5521" },
"receiver": { "name": "CF SHOP", "account": "0101XXXXXXX1234" },
"memo": "CF-10293"
},
"checks": {
"slip": { "status": "pass" },
"duplicate": { "status": "pass" },
"ref": { "status": "pass", "value": "00123456789" },
"receiver": { "status": "pass", "seen": "0101XXXXXXX1234" },
"amount": { "status": "pass", "expected": 150000, "seen": 150000 },
"time": { "status": "pass", "txn_at": "2026-10-31T20:50:00+07:00", "age_min": 10, "max_age_min": 30 },
"tamper": { "status": "pass", "score": 0, "signs": [] },
"high_amount": { "status": "pass" }
},
"review": null, // verdict = review → { status: "pending", reviewed_by, note, reviewed_at }
"created_at": "2026-10-31T13:59:58.120Z"
}
| status | ความหมาย | ควรทำ |
|---|---|---|
| accepted | ผ่านทุกชั้น หรือแอดมินยืนยันแล้ว | ยืนยันออเดอร์ |
| pending_review | ผ่านชั้นที่ปฏิเสธได้ แต่มีจุดต้องให้คนเช็คกับแอปธนาคาร | พักออเดอร์ รอ webhook หรือให้แอดมินกดยืนยัน |
| rejected | ไม่ผ่าน หรือแอดมินปฏิเสธ | แจ้งลูกค้าด้วย reasons[].message_th |
ชั้นตรวจ
แต่ละชั้นมี status = pass / review / fail / skip. มี fail อย่างน้อยหนึ่งชั้น → reject; ไม่มี fail แต่มี review → review; นอกนั้น → pass
| ชั้น | fail (reject) | review |
|---|---|---|
slip | ไม่ใช่สลิปที่สำเร็จ / อ่านยอดไม่ได้ | — |
duplicate | เลขอ้างอิงเคยใช้ในโปรเจกต์นี้ / ภาพเดิม / ไม่มีเลขแต่ยอด+เวลาเดิม | — |
ref | — | อ่านเลขอ้างอิงไม่ได้ |
receiver | บัญชีผู้รับไม่ตรงบัญชีร้าน (รองรับเลขที่ถูกปิดบางหลัก เช่น xxx-x-x6789-x, 0101XXXXXXX1234) | อ่านเลขบัญชีไม่ได้ |
amount | ยอดหรือสกุลเงินไม่ตรง expected_amount | อ่านสกุลเงินไม่ได้ |
time | เก่ากว่า max_age_min / อยู่ในอนาคตเกิน 5 นาที | อ่านเวลาไม่ได้ |
tamper | score ≥ tamper_reject_at (ปิดไว้เป็นค่าเริ่มต้น) | AI ให้คะแนนแต่งรูป ≥ 2 (0–3 พร้อมเหตุผลใน signs) |
high_amount | — | ยอดถึงเกณฑ์ review_min (เริ่มต้น 5,000,000 ₭ / 10,000 ฿ / 300 $) |
เลขอ้างอิงจะถูก "จอง" เฉพาะสลิปที่ได้ pass หรือ review — สลิปที่ถูก reject (เช่น ส่งผิดออเดอร์จนยอดไม่ตรง) ส่งใหม่กับออเดอร์ที่ถูกได้. สลิปที่แอดมินปฏิเสธยังจองเลขไว้ ใช้ซ้ำไม่ได้
รหัสเหตุผล
| code | ข้อความ |
|---|---|
not_a_slip | ภาพนี้ไม่ใช่สลิปโอนเงินที่สำเร็จ |
unreadable_amount | อ่านยอดเงินไม่ได้ — ส่งภาพแคปหน้าจอที่ชัดกว่านี้ |
duplicate_ref · duplicate_image · duplicate_slip | สลิปนี้เคยใช้แล้ว (checks.duplicate.duplicate_of = id เดิม) |
receiver_mismatch · receiver_unreadable | บัญชีผู้รับไม่ใช่ของร้าน / อ่านไม่ได้ |
amount_mismatch · currency_mismatch · currency_unreadable | ยอด/สกุลเงินไม่ตรงออเดอร์ |
txn_too_old · txn_in_future · txn_time_unreadable | เวลาโอนเกินกำหนด / อนาคต / อ่านไม่ได้ |
no_ref | อ่านเลขอ้างอิงไม่ได้ |
tamper_suspect · tamper_detected | AI เห็นร่องรอยการแต่งรูป |
high_amount | ยอดสูง ต้องให้แอดมินยืนยัน |
ยืนยันโดยแอดมิน
POST/v1/slips/{id}/review
ใช้กับสลิปที่ verdict = review และยังรออยู่เท่านั้น — ให้แอดมินเช็คเงินเข้าในแอปธนาคาร/SMS ก่อนกด. กดได้ครั้งเดียว
curl https://api.kuadslip.com/v1/slips/6c3a…/review \
-H "Authorization: Bearer $SLIP_API_KEY" -H "content-type: application/json" \
-d '{"decision":"approve","reviewer":"Nicky","note":"เช็คแอป BCEL แล้ว"}'
decision: approve | reject · reviewer จำเป็น · note ไม่บังคับ. ผลตอบกลับเหมือน verify พร้อม webhook: { id, status }
Webhook
เมื่อแอดมินยืนยันหรือปฏิเสธ API จะ POST ไปที่ webhook_url ของโปรเจกต์ (ตั้งด้วย PATCH /v1/project) ด้วย event slip.review.approved หรือ slip.review.rejected. ตอบ 2xx ภายใน 8 วินาที ถ้าพลาดจะลองซ้ำหลัง 1, 5, 30, 120, 360 นาที
POST /your/webhook
x-slip-event: slip.review.approved
x-slip-delivery: 9f1e… // ใช้กันประมวลผลซ้ำ
x-slip-signature: t=1793455200,v1=5d0c…
{ "id": "9f1e…", "type": "slip.review.approved", "created_at": "…", "data": { …ผลแบบเดียวกับ verify… } }
ตรวจลายเซ็น
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySlipWebhook(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // กัน replay
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
ใช้ raw body ตามที่ได้รับ (ก่อน JSON.parse). webhook_secret (whsec_…) ได้ครั้งแรกตอนตั้ง webhook_url และหมุนใหม่ได้ที่ POST /v1/project/webhook-secret
| Endpoint | ใช้ทำ |
|---|---|
POST /v1/webhooks/test | ส่ง event webhook.test ไปทดสอบ |
GET /v1/webhooks/deliveries?status=failed | ดูประวัติการส่ง |
POST /v1/webhooks/deliveries/{id}/redeliver | ส่งใหม่ไปที่ URL ปัจจุบัน |
ดูรายการสลิป
GET/v1/slips/{id} GET/v1/slips
ตัวกรอง: order_id, verdict, review_status=pending, ref, limit (≤100), before (ใส่ next_before เพื่อดูหน้าถัดไป)
ค่าตั้งโปรเจกต์
GET/v1/project PATCH/v1/project
{
"webhook_url": "https://cfpaylaos.com/api/slip-webhook",
"settings": {
"accounts": ["010112000001234"],
"currency": "LAK",
"max_age_min": 30,
"review_min": { "LAK": 5000000, "THB": 10000, "USD": 300 },
"tamper_review_at": 2,
"tamper_reject_at": null
}
}
ค่าใน request ของ verify (accounts, max_age_min, expected_currency) ทับค่าตั้งนี้เฉพาะครั้งนั้น
ยอดใช้งาน
GET/v1/usage?from=2026-10-01&to=2026-10-31
คืนจำนวน request / verify / ถูก rate limit รายวัน และสรุป pass / review / reject / รอยืนยัน / token ที่ใช้ (วันนับตามเวลา UTC+7)
Error & rate limit
{ "error": { "code": "rate_limited", "message": "Rate limit exceeded (60 requests/minute)" } }
| HTTP | code |
|---|---|
| 400 | image_required invalid_expected_amount invalid_currency invalid_decision … |
| 401 | unauthorized — key ผิดหรือถูกยกเลิก |
| 404 / 405 | not_found / method_not_allowed |
| 409 | not_reviewable already_reviewed |
| 413 / 415 | ภาพใหญ่เกิน / ไม่ใช่ภาพที่รองรับ |
| 429 | rate_limited (ดู Retry-After) · quota_exceeded |
| 502 | ai_unavailable — ตัวอ่านสลิปขัดข้องชั่วคราว ลองใหม่ได้ (ไม่บันทึกผล) |
ทุก response มี X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. ค่าเริ่มต้น 60 ครั้ง/นาที ต่อ key
แอดมินระบบ
ใช้ Authorization: Bearer $ADMIN_TOKEN — สำหรับเปิดโปรเจกต์และออก key ให้ลูกค้า (ยังไม่มีหน้าสมัครเอง)
| Endpoint | ใช้ทำ |
|---|---|
POST /v1/admin/tenants {name, contact, plan} | เพิ่มลูกค้า |
GET /v1/admin/tenants | ลูกค้าและโปรเจกต์ทั้งหมด |
POST /v1/admin/projects {tenant_id, name, settings, webhook_url, rate_per_min, monthly_quota} | สร้างโปรเจกต์ + key แรก (key แสดงครั้งเดียว) |
PATCH /v1/admin/projects/{id} | แก้ rate / quota / ปิดโปรเจกต์ |
GET|POST /v1/admin/projects/{id}/keys | ดู / ออก key ใหม่ |
DELETE /v1/admin/keys/{id} | ยกเลิก key |
GET /v1/admin/projects/{id}/usage | ยอดใช้งานของโปรเจกต์ |