คู่มือ API

เชื่อมต่อการตรวจสลิปโอนเงินเข้ากับเว็บไซต์ แอปพลิเคชัน บอทไลน์ หรือระบบหลังบ้านของคุณ — ส่งรูปสลิปเข้ามา ได้ผล JSON รูปแบบเดียวกันเสมอ

ภาพรวมและการเริ่มต้น#

API ของเราเป็น REST ส่งรูปด้วย multipart/form-data และตอบกลับเป็น JSON (UTF-8) ทุกคำขอต้องแนบ API key ผลลัพธ์หักโควต้าแพ็กเกจก่อน เมื่อโควต้าหมดจึงหักเครดิตสลิปละ 1 เครดิต — ใช้ชุดเดียวกับการตรวจผ่านหน้าเว็บ

Base URLใช้ทำอะไร
https://ppay-slip.comที่อยู่ของ API ทุก endpoint ด้านล่างต่อท้ายด้วย path ที่ระบุ
  1. 1

    สมัครสมาชิกและมีโควต้า/เครดิต — สมัครสมาชิก แล้วเลือกแพ็กเกจหรือทดลองใช้ฟรี (ถ้าเปิดให้)

  2. 2

    สร้าง API key ที่เมนู "API สำหรับนักพัฒนา" (ขึ้นต้นด้วย ppk_) — ระบบแสดงคีย์เต็มครั้งเดียวเท่านั้น

  3. 3

    ลองเรียก POST /api/v1/verify ด้วยรูปสลิปจริง (ดูตัวอย่างหัวข้อ "ตรวจสลิปแบบรอผล")

  4. 4

    ถ้าส่งสลิปจำนวนมาก ใช้แบบคิว + Webhook เพื่อไม่ต้องรอผลทีละใบ

การยืนยันตัวตน (API key)#

ส่ง API key ใน header Authorization แบบ Bearer (หรือ X-API-Key ก็ได้):

HTTP header
Authorization: Bearer ppk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

รักษา API key ให้ปลอดภัย

  • เรียก API จาก เซิร์ฟเวอร์ของคุณ เท่านั้น ห้ามใส่คีย์ในโค้ดเว็บ/แอปมือถือที่ผู้ใช้เปิดดูได้ (API นี้ไม่เปิด CORS จึงเรียกจากเบราว์เซอร์ไม่ได้อยู่แล้ว)
  • เก็บคีย์ใน environment variable หรือ secret manager ไม่ commit ลง Git
  • แยกคีย์ตามแอปพลิเคชัน หากรั่วไหลให้ เพิกถอน ในหน้า "API สำหรับนักพัฒนา" (มีผลทันที) แล้วสร้างใหม่
  • สร้างได้สูงสุด 10 คีย์ต่อบัญชี

คีย์ที่ไม่ถูกต้อง ถูกเพิกถอน หรือบัญชีถูกระงับ จะได้ 401 รหัส INVALID_API_KEY

ตรวจสลิปแบบรอผล#

POST https://ppay-slip.com/api/v1/verify — ส่งรูป 1 ใบ รอจนได้ผลแล้วตอบ HTTP 200 (โดยปกติใช้เวลาไม่กี่วินาที)

ฟิลด์ (multipart)จำเป็นรายละเอียด
fileใช่รูปสลิป PNG หรือ JPG ขนาดไม่เกิน 5MB — ใช้รูปต้นฉบับจากแอปธนาคาร (ภาพที่ถูกครอปหรือบีบอัดมากอาจอ่าน QR ไม่ได้)
queueไม่ใส่ never เพื่อบังคับให้ตอบผลตรง ๆ เสมอ (ดูหมายเหตุ 202 ด้านล่าง)
cURL
curl -X POST https://ppay-slip.com/api/v1/verify \
  -H "Authorization: Bearer ppk_xxxxxxxx" \
  -F "file=@slip.jpg"

HTTP 200 ไม่ได้แปลว่าสลิปถูกต้อง

HTTP 200 แปลว่า "ตรวจเสร็จแล้ว" ให้ดูฟิลด์ valid / code เพื่อรู้ผล เช่นสลิปปลอมหรือสลิปซ้ำก็ตอบ 200 แต่ valid: false

ช่วงที่ระบบหนาแน่น อาจตอบ 202

เมื่อมีงานค้างจำนวนมาก หรือผู้ให้บริการตรวจสลิปขัดข้องชั่วคราว ระบบจะรับสลิปเข้าคิวให้เอง แล้วตอบ HTTP 202 พร้อม queued: true และ jobId(หักโควต้า/เครดิตครั้งเดียว) จากนั้นรับผลทาง Webhook หรือดึงด้วย GET /api/v1/jobs/{jobId}โค้ดของคุณจึงควรรองรับทั้ง 200 และ 202 เสมอ

202 Accepted
{
  "ok": true,
  "queued": true,
  "jobId": "job_1f3a9c...",
  "status": "queued",
  "reference": null,
  "statusUrl": "/api/v1/jobs/job_1f3a9c..."
}

ตรวจสลิปแบบคิว (async / batch)#

เหมาะกับระบบที่ส่งสลิปจำนวนมาก หรือไม่อยากค้างรอผล — ระบบรับงานเข้าคิวแล้วตอบ HTTP 202 ทันที (โดยหักโควต้า/เครดิตตอนรับงาน) ประมวลผลเบื้องหลัง และส่งผลให้ทาง Webhook เมื่อเสร็จ

1) ส่งทีละใบ — POST /api/v1/verify/async

ฟิลด์จำเป็นรายละเอียด
fileใช่รูปสลิป PNG/JPG ไม่เกิน 5MB
referenceไม่เลขอ้างอิงของคุณ เช่น เลขออเดอร์ (1–64 ตัวอักษร A-Z a-z 0-9 . _ : -) ระบบส่งคืนให้ในผลลัพธ์และ Webhook เพื่อจับคู่กับรายการของคุณ
cURL
curl -X POST https://ppay-slip.com/api/v1/verify/async \
  -H "Authorization: Bearer ppk_xxxxxxxx" \
  -F "file=@slip.jpg" -F "reference=ORDER-1001"

2) ส่งหลายใบในคำขอเดียว — POST /api/v1/verify/batch

ส่งได้สูงสุด 10 ไฟล์ (รวมไม่เกิน 25MB) โดยซ้ำฟิลด์ file และ (ถ้าต้องการ) ซ้ำ reference ตามลำดับเดียวกัน แต่ละใบเป็นงานแยกกัน มี jobId ของตัวเอง

cURL
curl -X POST https://ppay-slip.com/api/v1/verify/batch \
  -H "Authorization: Bearer ppk_xxxxxxxx" \
  -F "file=@a.jpg" -F "file=@b.jpg" -F "file=@c.jpg" \
  -F "reference=A-1" -F "reference=A-2" -F "reference=A-3"
202 Accepted
{
  "ok": true,
  "accepted": 2,
  "rejected": 1,
  "jobs": [
    { "index": 0, "ok": true,  "jobId": "job_aa...", "reference": "A-1" },
    { "index": 1, "ok": false, "code": "INVALID_FILE", "error": "file must be a valid PNG/JPG up to 5MB" },
    { "index": 2, "ok": true,  "jobId": "job_bb...", "reference": "A-3" }
  ]
}
  • ไฟล์ที่ไม่ถูกต้องไม่ถูกหักเงิน และไม่กระทบใบอื่นในชุดเดียวกัน
  • ถ้าโควต้า/เครดิตหมดกลางชุด ใบที่เหลือจะถูกปฏิเสธด้วยรหัส INSUFFICIENT_BALANCE (ใบก่อนหน้าที่รับแล้วยังคงถูกประมวลผลตามปกติ)

3) ดึงผลของงาน — GET /api/v1/jobs/{jobId}

cURL
curl https://ppay-slip.com/api/v1/jobs/job_1f3a9c... \
  -H "Authorization: Bearer ppk_xxxxxxxx"
200 OK
{
  "ok": true,
  "jobId": "job_1f3a9c...",
  "reference": "ORDER-1001",
  "status": "done",                 // queued | processing | done
  "createdAt": "2026-10-12T07:31:58.120Z",
  "finishedAt": "2026-10-12T07:32:00.904Z",
  "result": { /* เมื่อ status = done: รูปแบบเดียวกับผลตรวจสลิป ดูหัวข้อถัดไป + jobId, reference */ }
}

ขณะ queued / processing ฟิลด์ result เป็น null — แนะนำให้ poll ห่างกัน 2–5 วินาที หรือใช้ Webhook แทนการ poll อ่านงานได้เฉพาะเจ้าของ API key เท่านั้น (งานของผู้อื่นจะได้ 404 JOB_NOT_FOUND)

ถ้าระบบตรวจสอบขัดข้อง คุณไม่ต้องทำอะไร

งานที่ผู้ให้บริการตรวจสลิปล่มจะถูกลองใหม่อัตโนมัติสูงสุด 3 ครั้ง หากยังไม่สำเร็จจะปิดงานเป็น SERVICE_UNAVAILABLE และ คืนโควต้า/เครดิตให้ทันที(billing.refunded: true)

รูปแบบผลลัพธ์#

ผลลัพธ์เป็นรูปแบบของเราเองเสมอ ไม่ว่าเบื้องหลังจะตรวจกับผู้ให้บริการรายใด ฟิลด์วันเวลาเป็น ISO-8601 (UTC) ยอดเงินเป็นบาท

ตัวอย่างผลลัพธ์ (สลิปถูกต้อง)
{
  "ok": true,
  "id": "b1c2d3e4-...",
  "valid": true,
  "result": "valid",
  "code": "VALID",
  "message": "สลิปถูกต้อง",
  "messageEn": "The slip is valid.",
  "slip": {
    "reference": "0158...9421",
    "amount": 1250.00,
    "currency": "THB",
    "transferredAt": "2026-10-12T07:32:00.000Z",
    "sender":   { "name": "นาย สมชาย ใจดี", "bank": "ธนาคารกสิกรไทย" },
    "receiver": { "name": "ร้านตัวอย่าง", "bank": "ธนาคารไทยพาณิชย์", "account": "xxx-x-x1234-x" }
  },
  "billing": { "paidWith": "quota", "refunded": false, "creditsLeft": 120, "quotaLeft": 480 }
}
ฟิลด์ความหมาย
validtrue = สลิปถูกต้อง (code เป็น VALID)
resultvalid ถูกต้อง · invalid สลิปไม่ผ่าน (ปลอม/ซ้ำ/ไม่พบ) · error ระบบขัดข้อง (ไม่ถูกหักเงิน)
codeรหัสผลลัพธ์ของเรา (ตารางถัดไป) — ใช้ในโค้ดของคุณ ไม่ควรเทียบกับข้อความ
message / messageEnข้อความอธิบายภาษาไทย / อังกฤษ แสดงให้ผู้ใช้ปลายทางได้
slipรายละเอียดสลิป (null เมื่อสลิปไม่ถูกต้อง): reference เลขอ้างอิงธนาคาร, amount, transferredAt, ผู้โอน, ผู้รับ (เลขบัญชีมักถูกปิดบางส่วน)
cachedtrue = ตอบจากบันทึกของเราโดยไม่ต้องตรวจกับธนาคารซ้ำ (เคยตรวจสลิปไฟล์เดียวกันนี้แล้ว) — ยังถูกหักโควต้า/เครดิตตามปกติ
previousเมื่อส่งสลิปที่ บัญชีคุณเคยตรวจแล้ว: checkedAt เวลาที่ตรวจครั้งแรก และ slip จะมีรายละเอียดเดิมให้ แต่ valid เป็น false และ code เป็น DUPLICATE_SLIP (สลิปของบัญชีอื่นจะไม่เห็นรายละเอียด)
billing.paidWithquota หักจากโควต้าแพ็กเกจ · credit หักจากเครดิต · unlimited บัญชีผู้ดูแลระบบ (ไม่หัก)
billing.refundedtrue เมื่อระบบขัดข้องและคืนโควต้า/เครดิตให้แล้ว
billing.creditsLeft / quotaLeftยอดคงเหลือหลังทำรายการ

อะไรที่ถูกหักเงิน?

สลิปที่ตรวจเสร็จแล้วถูกหักโควต้า/เครดิต 1 ครั้งต่อ 1 ใบ ไม่ว่าผลจะเป็นถูกต้อง ปลอม ซ้ำ หรือไม่พบ (เพราะเราต้องจ่ายค่าตรวจกับธนาคาร) ยกเว้นกรณี result: "error" ที่ระบบคืนให้อัตโนมัติ การส่งสลิปใบเดิมซ้ำจะได้ DUPLICATE_SLIP และถูกหักเพิ่มอีกครั้ง (ระบบตอบจากข้อมูลที่บันทึกไว้ได้ทันที จึงเร็วกว่าปกติ)

รหัสผลลัพธ์ (code)#

coderesultความหมายข้อความ (ไทย)
VALIDvalidสลิปถูกต้อง ยืนยันกับระบบธนาคารแล้วสลิปถูกต้อง
INVALID_SLIPinvalidสลิปไม่ถูกต้อง หรือเป็นสลิปปลอม/ตัดต่อสลิปไม่ถูกต้อง หรือเป็นสลิปปลอม
SLIP_NOT_FOUNDinvalidไม่พบรายการในระบบธนาคาร — อาจเพิ่งโอน (ลองใหม่ใน 1–2 นาที) หรืออ่าน QR ไม่ได้ไม่พบรายการนี้ในระบบธนาคาร (อาจเพิ่งโอน ลองใหม่ใน 1-2 นาที) หรืออ่าน QR บนสลิปไม่ได้
DUPLICATE_SLIPinvalidสลิปใบนี้เคยถูกตรวจ/ใช้ไปแล้วสลิปนี้ถูกใช้ตรวจสอบไปแล้ว
SERVICE_UNAVAILABLEerrorผู้ให้บริการตรวจสลิปขัดข้อง — คืนโควต้า/เครดิตแล้ว ลองใหม่ภายหลังระบบตรวจสอบสลิปขัดข้องชั่วคราว กรุณาลองใหม่
NOT_CONFIGUREDerrorฝั่งผู้ดูแลระบบยังไม่ได้ตั้งค่าผู้ให้บริการตรวจสลิป — คืนโควต้า/เครดิตแล้วระบบตรวจสอบสลิปยังไม่พร้อมใช้งาน
INTERNAL_ERRORerrorข้อผิดพลาดภายใน — คืนโควต้า/เครดิตแล้วระบบขัดข้อง กรุณาลองใหม่อีกครั้ง

ข้อผิดพลาดของคำขอ#

เมื่อคำขอไม่สามารถรับได้ จะตอบ HTTP status ที่ไม่ใช่ 2xx พร้อม { "ok": false, "code": "...", "error": "..." } (ไม่มีการหักโควต้า/เครดิต)

HTTPcodeสาเหตุ / วิธีแก้
400INVALID_FILEไม่มีไฟล์ หรือไม่ใช่รูป PNG/JPG ที่ถูกต้อง (ตรวจจากเนื้อไฟล์จริง ไม่ใช่นามสกุล) หรือใหญ่เกิน 5MB
400INVALID_REFERENCEreference ไม่ตรงรูปแบบ (1–64 ตัวอักษร A-Z a-z 0-9 . _ : -)
400INVALID_FILE_COUNTbatch ต้องส่ง 1–10 ไฟล์
401INVALID_API_KEYไม่มีคีย์ คีย์ผิด/ถูกเพิกถอน หรือบัญชีถูกระงับ
402INSUFFICIENT_BALANCEโควต้าและเครดิตหมด — ซื้อแพ็กเกจหรือเติมเครดิต
404JOB_NOT_FOUNDไม่พบงานนี้ (หรือไม่ใช่งานของคีย์นี้ / งานเก่าเกิน 30 วันถูกลบแล้ว)
413FILE_TOO_LARGEคำขอใหญ่เกินกำหนด หรือไม่ได้ส่งเป็น multipart ปกติ (ต้องมี Content-Length)
429RATE_LIMITEDเรียกถี่เกินไป — รอตามค่า header Retry-After (วินาที) แล้วลองใหม่
429QUEUE_FULLมีงานค้างในคิวของบัญชีครบ 100 งาน — รอให้ประมวลผลเสร็จก่อน
503STORAGE_UNAVAILABLE / QUEUE_UNAVAILABLEระบบจัดเก็บไฟล์/คิวขัดข้องชั่วคราว (ไม่หักเงิน) — ลองใหม่

Webhook#

แทนที่จะ poll ผลของงานแบบคิว ให้ระบบ ส่งผลมาหาคุณ เมื่อตรวจเสร็จ ตั้งค่าที่เมนู "API สำหรับนักพัฒนา" → Webhook:

  1. 1

    ใส่ URL ปลายทางของคุณ — ต้องเป็น https และเป็นโดเมนสาธารณะ (ไม่รับ localhost / IP ภายใน)

  2. 2

    บันทึก แล้วคัดลอก คีย์ลายเซ็น (whsec_...) เก็บไว้ — แสดงครั้งเดียว หากหายให้กด "สร้างคีย์ใหม่"

  3. 3

    กด "ส่งทดสอบ" เพื่อดูว่าเซิร์ฟเวอร์ของคุณรับและตรวจลายเซ็นได้

เหตุการณ์ที่ส่ง:

typeเมื่อไหร่
verification.completedงานแบบคิว (async / batch / ที่ระบบรับเข้าคิวเองตอนหนาแน่น) ตรวจเสร็จ — ทั้งผลถูกต้อง ไม่ถูกต้อง และข้อผิดพลาดที่คืนเงินแล้ว
webhook.testเมื่อกด “ส่งทดสอบ”
POST ไปที่ URL ของคุณ
POST /webhooks/ppay HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: PPaySlip-Webhook/1.0
X-PPay-Event: verification.completed
X-PPay-Delivery: evt_9b1c2d...
X-PPay-Attempt: 1
X-PPay-Signature: t=1760254320,v1=5d41402abc4b2a76b9719d911017c592...

{
  "id": "evt_9b1c2d...",
  "type": "verification.completed",
  "apiVersion": "1",
  "createdAt": "2026-10-12T07:32:00.904Z",
  "data": {
    "jobId": "job_1f3a9c...",
    "reference": "ORDER-1001",
    "ok": true,
    "valid": true,
    "result": "valid",
    "code": "VALID",
    "message": "สลิปถูกต้อง",
    "slip": { "reference": "0158...9421", "amount": 1250.00, "currency": "THB", "...": "..." },
    "billing": { "paidWith": "quota", "refunded": false, "creditsLeft": 120, "quotaLeft": 480 }
  }
}

เซิร์ฟเวอร์ของคุณต้องทำอะไร

  • ตอบ HTTP 2xx ภายใน 8 วินาที เพื่อยืนยันว่าได้รับแล้ว (ควรตอบทันทีแล้วค่อยประมวลผลต่อเบื้องหลัง)
  • ตรวจลายเซ็นทุกครั้งก่อนเชื่อถือข้อมูล (หัวข้อถัดไป)
  • ถ้าไม่ตอบ 2xx (รวมถึง 3xx — ระบบไม่ตามการ redirect) จะถือว่าล้มเหลวและส่งซ้ำ

การส่งซ้ำ (retry)

ครั้งที่ล้มเหลวส่งซ้ำอีกครั้งใน
11 นาที
25 นาที
330 นาที
42 ชั่วโมง
56 ชั่วโมง
6เลิกส่งอัตโนมัติ (สถานะ “ล้มเหลว”) — กด “ส่งซ้ำ” เองได้ในหน้า API สำหรับนักพัฒนา

อาจได้รับเหตุการณ์เดิมซ้ำ

ระบบรับประกัน "ส่งอย่างน้อยหนึ่งครั้ง" (at-least-once) — ถ้าคุณตอบ 2xx ช้าเกินหรือเครือข่ายหลุด อาจได้รับเหตุการณ์เดิมซ้ำ ให้ใช้ฟิลด์ id ของเหตุการณ์ (หรือ data.jobId) เก็บว่าประมวลผลไปแล้วหรือยัง เพื่อไม่ให้ทำรายการซ้ำ (เช่น ไม่ปรับสถานะออเดอร์ซ้ำ)

ตรวจลายเซ็น Webhook#

ทุกคำขอมี header X-PPay-Signature: t=<unix seconds>,v1=<hex> โดย v1 คือ HMAC-SHA256 ของข้อความ "<t>.<raw body>" ด้วยคีย์ลายเซ็น (whsec_...) ของคุณ

สำคัญ

  • ต้องใช้ raw body ตามที่ได้รับ (ก่อน parse JSON) เพราะแค่ช่องว่างต่างกันลายเซ็นก็ไม่ตรง
  • เทียบด้วยฟังก์ชัน constant-time (timingSafeEqual / hash_equals / compare_digest)
  • ปฏิเสธคำขอที่ t ต่างจากเวลาปัจจุบันเกิน 5 นาที เพื่อกันการนำคำขอเก่ามาส่งซ้ำ (replay)
Node.js (Express)
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.PPAY_WEBHOOK_SECRET; // whsec_...
const app = express();

function verify(raw, header, secret) {
  const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;       // replay protection
  const expected = createHmac("sha256", secret).update(parts.t + "." + raw).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

// express.raw keeps the body exactly as received
app.post("/webhooks/ppay", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body.toString("utf8"), req.get("x-ppay-signature"), SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);                                   // acknowledge first, then work
  if (event.type === "verification.completed") {
    // event.data.reference = your order number, event.data.valid = true/false
  }
});
PHP
<?php
$secret = getenv("PPAY_WEBHOOK_SECRET");
$raw    = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_PPAY_SIGNATURE"] ?? "";

parse_str(str_replace(",", "&", $header), $p);           // t=...&v1=...
$expected = hash_hmac("sha256", ($p["t"] ?? "") . "." . $raw, $secret);

if (!isset($p["t"], $p["v1"]) || !hash_equals($expected, $p["v1"]) || abs(time() - (int)$p["t"]) > 300) {
    http_response_code(400);
    exit;
}
http_response_code(200);
$event = json_decode($raw, true);
Python (Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort

SECRET = os.environ["PPAY_WEBHOOK_SECRET"]
app = Flask(__name__)

def verify(raw: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", "")) and abs(time.time() - int(parts["t"])) <= 300

@app.post("/webhooks/ppay")
def hook():
    raw = request.get_data()                              # raw bytes, before JSON parsing
    if not verify(raw, request.headers.get("X-PPay-Signature", ""), SECRET):
        abort(400)
    event = request.get_json()
    return "", 200

ตัวอย่างโค้ด#

Node.js 18+ — ตรวจแบบรอผล (รองรับ 202)

Node.js
import { readFile } from "node:fs/promises";

async function verifySlip(path) {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)], { type: "image/jpeg" }), "slip.jpg");

  const res = await fetch("https://ppay-slip.com/api/v1/verify", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.PPAY_API_KEY },
    body: form,
  });
  const data = await res.json();

  if (res.status === 202) return { pending: true, jobId: data.jobId };   // busy: wait for the webhook / poll the job
  if (!res.ok) throw new Error(data.code + ": " + data.error);          // 401, 402, 429 ...
  return { pending: false, valid: data.valid, code: data.code, slip: data.slip };
}

Node.js — ส่งเข้าคิวพร้อมเลขออเดอร์ แล้วรอ Webhook

Node.js
async function queueSlip(path, orderNo) {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)], { type: "image/jpeg" }), "slip.jpg");
  form.append("reference", orderNo);                                     // comes back in the webhook

  const res = await fetch("https://ppay-slip.com/api/v1/verify/async", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.PPAY_API_KEY },
    body: form,
  });
  if (res.status !== 202) throw new Error((await res.json()).code);
  return (await res.json()).jobId;                                       // store it next to the order
}

PHP (cURL)

PHP
<?php
$ch = curl_init("https://ppay-slip.com/api/v1/verify");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Authorization: Bearer " . getenv("PPAY_API_KEY")],
    CURLOPT_POSTFIELDS     => ["file" => new CURLFile("slip.jpg", "image/jpeg", "slip.jpg")],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$data   = json_decode($body, true);

if ($status === 202) { /* queued: use $data["jobId"] */ }
elseif ($status === 200 && $data["valid"]) { echo $data["slip"]["amount"]; }

Python (requests)

Python
import os, requests

r = requests.post(
    "https://ppay-slip.com/api/v1/verify",
    headers={"Authorization": "Bearer " + os.environ["PPAY_API_KEY"]},
    files={"file": ("slip.jpg", open("slip.jpg", "rb"), "image/jpeg")},
    timeout=30,
)
data = r.json()
if r.status_code == 202:
    job_id = data["jobId"]            # queued
elif r.ok and data["valid"]:
    print(data["slip"]["amount"])

ผูกการตรวจสลิปกับร้านค้า (shop)#

ถ้าคุณสร้างร้านค้าและผูกบัญชีรับเงินไว้ในเมนู ร้านค้า & รับชำระเงิน คุณเลือกได้ว่าจะให้ระบบตรวจ ผู้รับในสลิป (เลขบัญชีและชื่อบัญชีที่ผูกไว้) หรือไม่ — ส่งฟิลด์ shop (รหัสร้าน หรือที่อยู่ร้าน) พร้อมรูปสลิป ใช้ได้กับ /api/v1/verify, /verify/async และ /verify/batch

การเรียกผลที่ได้
ไม่ส่ง shopตรวจสลิปทั่วไป: ยืนยันกับธนาคารว่าสลิปเป็นของจริงและไม่ซ้ำ โดยไม่สนว่าโอนเข้าใคร (เหมือนเดิมทุกประการ)
shop=my-shopตรวจเหมือนข้างต้น และ ต้องโอนเข้าบัญชีใดบัญชีหนึ่งของร้านนั้น ถ้าไม่ตรงจะได้ valid: false และ code: RECEIVER_MISMATCH
cURL
curl -X POST https://ppay-slip.com/api/v1/verify \
  -H "Authorization: Bearer ppk_xxxxxxxx" \
  -F "file=@slip.jpg" \
  -F "shop=my-shop"
ฟิลด์เพิ่มในผลลัพธ์ เมื่อส่ง shop
"shop": {
  "id": "7c1f...",
  "name": "ร้านของฉัน",
  "matched": true,          // true = โอนเข้าบัญชีของร้าน · false = โอนเข้าที่อื่น · null = ยังบอกไม่ได้
  "account": { "id": "a91c...", "label": "ธนาคารกสิกรไทย" }
}
  • บัญชีของร้านที่กรอก “ชื่อบัญชี” ไว้ จะถูกเทียบทั้งเลขบัญชี (ตัวเลขที่มองเห็นบนสลิป) และชื่อผู้รับ ถ้าไม่กรอกชื่อ จะเทียบเลขบัญชีอย่างเดียว
  • สลิปที่ผู้รับไม่ตรงจะ ไม่ถูกบันทึกว่าใช้แล้ว จึงนำไปตรวจกับร้านอื่นได้ แต่ยังนับเป็นการตรวจ 1 ครั้ง
  • ข้อผิดพลาดก่อนหักเงิน: 400 SHOP_NOT_FOUND (ไม่มีร้านนี้ในบัญชีคุณ) หรือ SHOP_NO_ACCOUNT (ร้านยังไม่ได้ผูกบัญชีรับเงิน)
  • หน้าเว็บ “ตรวจสอบสลิป” มีตัวเลือกร้านเช่นเดียวกัน (เลือก “ไม่ผูกกับร้าน” เพื่อตรวจทั่วไป)

ใบแจ้งชำระเงิน (invoices) และ Webhook invoice.paid#

สร้างรายการรับชำระให้ลูกค้าของคุณจากระบบของคุณเอง แล้วส่งลิงก์ payUrl ให้ลูกค้า — ลูกค้าสแกน QR พร้อมเพย์ที่มียอดเงินอยู่แล้ว โอนเงิน และอัปโหลดสลิปที่หน้านั้น ระบบตรวจสลิปให้ (ผู้รับต้องเป็นบัญชีของร้าน ยอดต้องไม่ต่ำกว่ายอดรายการ และสลิปต้องทำรายการหลังสร้างรายการ) แล้วปิดรายการเป็น paid ให้อัตโนมัติ

Endpointใช้ทำอะไร
POST /api/v1/invoicesสร้างรายการ body: shop, amount (1–1,000,000 ทศนิยมไม่เกิน 2 ตำแหน่ง), title, reference (เลขที่คำสั่งซื้อของคุณ), expiresInHours (1–720, ค่าเริ่มต้น 24) — ไม่หักโควต้า
GET /api/v1/invoices/{code}ดูสถานะรายการ (pending / paid / expired / cancelled)
GET /api/v1/invoices?shop=&status=รายการล่าสุด 100 รายการ กรองตามร้าน/สถานะได้
สร้างรายการ
curl -X POST https://ppay-slip.com/api/v1/invoices \
  -H "Authorization: Bearer ppk_xxxxxxxx" -H "Content-Type: application/json" \
  -d '{"shop":"my-shop","amount":590,"reference":"ORDER-1001","title":"สั่งซื้อสินค้า"}'
201 Created
{
  "ok": true,
  "invoice": {
    "code": "k3n8xq2m7vbt4h9d",
    "status": "pending",
    "amount": 590,
    "currency": "THB",
    "reference": "ORDER-1001",
    "payUrl": "https://ppay-slip.com/pay/k3n8xq2m7vbt4h9d",
    "expiresAt": "2026-10-13T08:00:00.000Z",
    "paidAt": null
  }
}

เมื่อลูกค้าชำระสำเร็จ ระบบส่ง Webhook (ถ้าคุณตั้งไว้) เหตุการณ์ invoice.paid พร้อมลายเซ็นเหมือนเหตุการณ์อื่น:

invoice.paid
{
  "id": "evt_...", "type": "invoice.paid", "apiVersion": "1", "createdAt": "...",
  "data": {
    "invoice": { "code": "k3n8xq2m7vbt4h9d", "reference": "ORDER-1001", "amount": 590, "paidAt": "..." },
    "shop": { "id": "7c1f...", "name": "ร้านของฉัน" },
    "slip": { "reference": "0158...", "amount": 590, "transferredAt": "...", "sender": {...}, "receiver": {...} }
  }
}

ค่าตรวจสลิปของรายการรับชำระ

สลิปที่ลูกค้าอัปโหลดแต่ละใบนับเป็นการตรวจ 1 ครั้งในบัญชีของคุณ (หักโควต้า/เครดิตตามปกติ) แต่ละรายการรับอัปโหลดได้สูงสุด 5 ครั้ง เพื่อไม่ให้ใครยิงสลิปมั่วจนยอดของคุณหมด

สร้าง QR พร้อมเพย์#

GET|POST https://ppay-slip.com/api/v1/promptpay — ใส่เบอร์พร้อมเพย์ (หรือเลขบัตรประชาชน/ผู้เสียภาษี) และจำนวนเงิน แล้วได้ QR มาตรฐาน PromptPay ให้ลูกค้าสแกนชำระ ไม่หักโควต้าหรือเครดิต แต่จำกัด 120 ครั้ง/นาที ต่อบัญชี

พารามิเตอร์จำเป็นรายละเอียด
targetใช่เบอร์มือถือ 10 หลักที่ขึ้นต้น 06/08/09 (ใส่ - หรือ +66 ได้) หรือเลข 13 หลัก (บัตรประชาชน/ผู้เสียภาษี) หรือ e-Wallet 15 หลัก
amountไม่จำนวนเงินบาท 0.01 – 1,000,000 ทศนิยมไม่เกิน 2 ตำแหน่ง ถ้าไม่ส่ง = QR ที่ให้ผู้จ่ายกรอกยอดเอง
formatไม่`json` (ค่าเริ่มต้น) หรือ svg เพื่อรับรูป QR กลับมาตรง ๆ
cURL
curl -X POST https://ppay-slip.com/api/v1/promptpay \
  -H "Authorization: Bearer ppk_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"target":"081-234-5678","amount":100.50}'
200 OK
{
  "ok": true,
  "payload": "00020101021229370016A0000006770101110113006681234567853037645406100.505802TH6304XXXX",
  "amount": 100.5,
  "currency": "THB",
  "target": { "type": "phone", "masked": "081xxxx678" },
  "qr": { "svg": "<svg ...>", "dataUrl": "data:image/svg+xml;base64,..." }
}
  • qr.dataUrl ใส่ใน <img src> ได้ทันที หรือใช้ payload สร้าง QR ด้วยไลบรารีของคุณเอง
  • ข้อผิดพลาด: 400 รหัส INVALID_TARGET / INVALID_AMOUNT / INVALID_FORMAT
  • QR นี้เป็นเพียงข้อมูลสำหรับสแกน ระบบไม่ได้ตรวจว่าลูกค้าชำระแล้ว — ให้ลูกค้าส่งสลิปมาตรวจด้วย /api/v1/verify

ข้อจำกัดและโควต้า#

รายการค่า
ขนาดรูปไม่เกิน 5MB ต่อใบ — เฉพาะ PNG / JPG
ความถี่การเรียก60 ใบ/นาที ต่อบัญชี (รวม verify, async และ batch) — เกินจะได้ 429 พร้อม Retry-After
batchสูงสุด 10 ไฟล์ และรวมไม่เกิน 25MB ต่อคำขอ
งานค้างในคิวสูงสุด 100 งานต่อบัญชี (queued + processing)
ความหนาแน่นระบบสลับเป็นเข้าคิวเองเมื่อมีงานค้างตั้งแต่ 30 งานขึ้นไปทั้งระบบ หรือผู้ให้บริการตรวจสลิปทุกรายขัดข้อง
ลองใหม่ของงานคิวสูงสุด 3 ครั้งเมื่อผู้ให้บริการขัดข้อง (รอ 30 วินาที แล้ว 2 นาที) จากนั้นคืนโควต้า/เครดิต
เก็บผลงานคิว30 วัน (ประวัติ Webhook 14 วัน)
Webhookตอบ 2xx ภายใน 8 วินาที · ส่งซ้ำสูงสุด 6 ครั้งรวม · URL ต้องเป็น https สาธารณะ
API keyได้สูงสุด 10 คีย์ต่อบัญชี
ค่าตรวจโควต้าแพ็กเกจก่อน เมื่อหมดหักเครดิต 1 เครดิต/ใบ

แนวทางที่แนะนำ#

  • จับคู่กับออเดอร์ด้วย reference — ส่ง reference (เลขออเดอร์) ทุกครั้งที่ใช้แบบคิว แล้วใช้ค่านี้ใน Webhook เพื่ออัปเดตออเดอร์ถูกรายการ
  • เทียบยอดเงินและผู้รับเอง — API ยืนยันว่าสลิปเป็นของจริงและยังไม่เคยใช้ แต่การตัดสินว่า "ยอด/บัญชีผู้รับถูกต้องตามออเดอร์หรือไม่" เป็นหน้าที่ของระบบคุณ ให้เทียบ slip.amount และ slip.receiver กับออเดอร์ก่อนอนุมัติ
  • รองรับ 202 เสมอ แม้ใช้ endpoint แบบรอผล เพราะช่วงหนาแน่นระบบอาจรับเข้าคิวให้
  • ทำให้ตัวจัดการ Webhook เป็น idempotent — เก็บ id เหตุการณ์ที่ประมวลผลแล้ว
  • อย่าลองซ้ำอัตโนมัติเมื่อได้ผล invalid — การส่งสลิปใบเดิมซ้ำให้ผล DUPLICATE_SLIP และถูกหักเพิ่ม ให้ลองซ้ำเฉพาะเมื่อ result: "error" (คืนเงินแล้ว) หรือ HTTP 5xx/429
  • เก็บ log ของ id / jobId ที่ได้รับ เพื่อใช้ตรวจสอบย้อนหลังร่วมกับหน้า "ประวัติ" ในพื้นที่สมาชิก
  • ติดปัญหา? แจ้งทีมงานพร้อมแนบ id หรือ jobId ของรายการ