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)

ฟิลด์ชนิดคำอธิบาย
imagefileภาพสลิป (multipart) — หรือ image_base64 ใน JSON
expected_amountnumberยอดของออเดอร์ ต้องตรงกับยอดในสลิป ไม่ส่ง = ไม่ตรวจยอด
expected_currencyLAK | THB | USDค่าเริ่มต้น = currency ของโปรเจกต์ (LAK)
accountsstring[] หรือ "a,b"บัญชีผู้รับของร้าน (เลขเต็ม หรือ 4 ตัวท้าย) ทับค่าตั้งโปรเจกต์
max_age_minnumberสลิปต้องโอนไม่เกินกี่นาทีก่อนเรียก API (ค่าเริ่มต้น 30, 0 = ไม่จำกัด)
order_idstringรหัสออเดอร์ของคุณ ใช้ค้นหา และทำให้การส่งซ้ำ (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 นาทีอ่านเวลาไม่ได้
tamperscore ≥ 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_detectedAI เห็นร่องรอยการแต่งรูป
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)" } }
HTTPcode
400image_required invalid_expected_amount invalid_currency invalid_decision …
401unauthorized — key ผิดหรือถูกยกเลิก
404 / 405not_found / method_not_allowed
409not_reviewable already_reviewed
413 / 415ภาพใหญ่เกิน / ไม่ใช่ภาพที่รองรับ
429rate_limited (ดู Retry-After) · quota_exceeded
502ai_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ยอดใช้งานของโปรเจกต์