guides

ตัวอย่างพร้อมใช้

โค้ดก๊อปไปวางแล้วใช้ได้จริง — ไคลเอนต์ที่ retry เอง, รับสลิปจากลูกค้าแล้วปิดออเดอร์, ส่งสลิปแบบ multipart/base64/URL/QR, ทดสอบ webhook ในเครื่อง

⏱ Copy & paste

โค้ดในหน้านี้ รันได้จริงทุกบล็อก ก๊อปไปวางแล้วแก้แค่ส่วนฐานข้อมูลของคุณ เลือกภาษาที่แท็บครั้งเดียว ทุกบล็อกจะสลับตาม

ต้องการไปที่
ฟังก์ชันตรวจสลิปที่ retry / timeout ให้เอง1. ไคลเอนต์พร้อมใช้
ลูกค้าอัปโหลดสลิป → ตรวจ → ปิดออเดอร์2. รับสลิปแล้วปิดออเดอร์
ส่งแบบ form upload / base64 / URL / ข้อความ QR3. รูปอยู่ในรูปแบบอื่น
ลอง webhook โดยยังไม่ขึ้นเซิร์ฟเวอร์จริง4. ทดสอบ webhook ในเครื่อง

ทุกตัวอย่างอ่านคีย์จาก environment variable — ช่วงพัฒนาใช้ sk_test_ ได้ ไม่กินโควตาของร้าน

Terminal
export SLIPBOLT_API_KEY="sk_test_..."

1. ไคลเอนต์พร้อมใช้

ไฟล์เดียว ไม่มี dependency เพิ่ม (Python ใช้ requests) — จัดการ 3 เรื่องที่ทุกระบบจริงต้องมี:

  • timeout 30 วินาที — ไม่ค้างทั้ง request ของลูกค้าเมื่อเน็ตมีปัญหา
  • retry อัตโนมัติ เมื่อเน็ตหลุด · 429 (รอตาม Retry-After) · 5xx — ปลอดภัยเพราะส่งรูปเดิมซ้ำใน 24 ชม. ได้ผลเดิมกลับมา (cached: true) ไม่ตรวจซ้ำ ไม่หักโควตาซ้ำ
  • error ที่มี status + code — แยก "โควตาหมด" (402) ออกจาก "คีย์ผิด" (401) ได้โดยไม่ต้องอ่านข้อความ
// slipbolt.js — Node.js 18+ · ไม่ต้องลงแพ็กเกจเพิ่ม
const BASE = 'https://api.slip-bolt.com/api/v1';

export class SlipBoltError extends Error {
  constructor(status, body) {
    super(`${status} ${body.code ?? body.error ?? ''}: ${body.message ?? 'request failed'}`);
    this.status = status; // HTTP status เช่น 402
    this.code = body.code; // เช่น QUOTA_EXCEEDED
    this.requestId = body.requestId; // แนบมาตอนติดต่อทีมงาน
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

/** ตรวจสลิป 1 ใบ — คืนก้อน `data` · โยน SlipBoltError เมื่อ API ตอบ error */
export async function verifySlip(image, { apiKey = process.env.SLIPBOLT_API_KEY, retries = 3 } = {}) {
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await fetch(`${BASE}/verify`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/octet-stream' },
        body: image,
        signal: AbortSignal.timeout(30_000),
      });
    } catch (err) {
      if (attempt >= retries) throw err; // เน็ตหลุด / timeout ครบจำนวนครั้งแล้ว
      await sleep(500 * 2 ** attempt);
      continue;
    }

    if ((res.status === 429 || res.status >= 500) && attempt < retries) {
      await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
      continue;
    }

    const body = await res.json().catch(() => ({}));
    if (!res.ok) throw new SlipBoltError(res.status, body);
    return body.data; // ผลอยู่ใน data เสมอ
  }
}

เรียกใช้:

import { readFile } from 'node:fs/promises';
import { verifySlip } from './slipbolt.js';

const slip = await verifySlip(await readFile('slip.jpg'));
console.log(slip.status, slip.amount, slip.transRef);

2. รับสลิปแล้วปิดออเดอร์

โจทย์ที่เจอบ่อยที่สุด: ลูกค้าอัปโหลดสลิปในหน้าชำระเงิน → เซิร์ฟเวอร์คุณตรวจ → ปิดออเดอร์อัตโนมัติ

เรียก SlipBolt จากเซิร์ฟเวอร์ของคุณเท่านั้น — ห้ามยิงจาก JavaScript ในเบราว์เซอร์หรือแอปมือถือโดยตรง เพราะ API key จะหลุดไปกับโค้ดฝั่งลูกค้า · เบราว์เซอร์ส่งรูปให้เซิร์ฟเวอร์คุณ แล้วเซิร์ฟเวอร์คุณค่อยส่งต่อ

ปล่อยของได้เมื่อผ่านครบ 4 ข้อ

#เช็คถ้าข้าม
1verified === trueรับสลิปปลอม / สลิปที่โอนเข้าบัญชีคนอื่น
2duplicate === falseลูกค้าส่งสลิปเก่าที่เคยใช้แล้วมาอีกรอบ
3amount ตรงกับยอดออเดอร์ลูกค้าโอน 1 บาทแล้วส่งสลิปมา — สลิปก็ยัง "จริง"
4transRef ยังไม่เคยผูกกับออเดอร์อื่น ในระบบคุณสลิปใบเดียวปิดได้สองออเดอร์

ข้อ 4 ข้ามไม่ได้ — ส่งรูปเดิมซ้ำภายใน 24 ชม. ระบบคืนผลเดิม (cached: true · verified: true · duplicate: false) เพื่อให้ retry ได้ปลอดภัย ⇒ ถ้าลูกค้าเอารูปเดียวกันไปอัปโหลดกับอีกออเดอร์ ข้อ 1–3 จะผ่านหมด ตัวกันจริงคือ คอลัมน์ transRef ที่ติด UNIQUE ในตารางออเดอร์ของคุณ

หน้าเว็บ (ฝั่งลูกค้า)

ส่งไฟล์เป็น body ตรง ๆ ไม่ต้องใช้ FormData — ใช้ได้กับเซิร์ฟเวอร์ทั้งสามภาษาด้านล่าง

HTML
<input type="file" id="slip" accept="image/*">
<p id="result"></p>

<script>
  document.getElementById('slip').addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    // PHP ใช้ '/pay.php?order=1001' แทน
    const res = await fetch('/orders/1001/slip', {
      method: 'POST',
      headers: { 'Content-Type': file.type },
      body: file,
    });
    const out = await res.json();
    document.getElementById('result').textContent = out.message;
  });
</script>

เซิร์ฟเวอร์

ใช้ไฟล์ไคลเอนต์จากข้อ 1 · ตัวอย่าง Node.js / Python เก็บออเดอร์ในหน่วยความจำให้รันได้ทันที — ของจริงเปลี่ยนเป็นตารางในฐานข้อมูลของคุณ (ตัวอย่าง PHP ใช้ PDO ให้ดูเป็นแนว)

// server.js — npm install express
import express from 'express';
import { verifySlip } from './slipbolt.js';

const app = express();

// ของจริง = ตารางในฐานข้อมูล · usedRefs = คอลัมน์ transRef ที่ติด UNIQUE
const orders = new Map([['1001', { total: 250, paid: false }]]);
const usedRefs = new Set();

// ข้อความที่บอกลูกค้าเมื่อสลิปไม่ผ่าน
const REJECT = {
  DUPLICATE: 'สลิปนี้ถูกใช้ไปแล้ว',
  FRAUD: 'สลิปไม่ถูกต้อง กรุณาติดต่อร้าน',
  RECEIVER_MISMATCH: 'สลิปนี้ไม่ได้โอนเข้าบัญชีของร้าน',
  INVALID: 'อ่านสลิปไม่ได้ ลองอัปโหลดรูปที่เห็น QR ชัด ๆ',
  PROVIDER_ERROR: 'ระบบตรวจสลิปขัดข้องชั่วคราว ลองส่งใหม่อีกครั้ง',
};

app.post('/orders/:id/slip', express.raw({ type: 'image/*', limit: '5mb' }), async (req, res) => {
  const order = orders.get(req.params.id);
  if (!order) return res.status(404).json({ ok: false, message: 'ไม่พบออเดอร์' });
  if (order.paid) return res.json({ ok: true, message: 'ออเดอร์นี้ชำระแล้ว' });
  if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
    return res.status(400).json({ ok: false, message: 'กรุณาแนบรูปสลิป' });
  }

  let slip;
  try {
    slip = await verifySlip(req.body);
  } catch (err) {
    console.error('SlipBolt:', err); // 402 = โควตาร้านหมด · 401/403 = คีย์หรือแพ็ก → แจ้งเตือนทีมคุณ
    return res.status(503).json({ ok: false, message: 'ตรวจสลิปอัตโนมัติไม่ได้ชั่วคราว ลองใหม่หรือติดต่อร้าน' });
  }

  // 1) สลิปจริงและเข้าบัญชีร้าน  2) ไม่ใช่สลิปที่เคยตรวจแล้ว
  if (!slip.verified || slip.duplicate) {
    return res.status(422).json({ ok: false, message: REJECT[slip.status] ?? 'สลิปไม่ผ่านการตรวจ' });
  }
  // 3) ยอดตรงกับออเดอร์ — API ไม่รู้ว่าออเดอร์นี้ต้องจ่ายเท่าไร
  if (Math.abs(slip.amount - order.total) > 0.001) {
    return res.status(422).json({
      ok: false,
      message: `ยอดโอน ${slip.amount} บาท ไม่ตรงกับยอดออเดอร์ ${order.total} บาท`,
    });
  }
  // 4) สลิปใบเดียวปิดได้ออเดอร์เดียว
  if (usedRefs.has(slip.transRef)) {
    return res.status(422).json({ ok: false, message: 'สลิปนี้ถูกใช้กับออเดอร์อื่นแล้ว' });
  }

  usedRefs.add(slip.transRef);
  order.paid = true;
  order.transRef = slip.transRef;
  res.json({ ok: true, message: 'ชำระเงินเรียบร้อย ขอบคุณค่ะ', transRef: slip.transRef });
});

app.listen(3000);

อยากได้ผลแบบไม่ต้องให้ลูกค้ารอ? ลูกค้าส่งสลิปใน LINE OA ของร้านแล้วให้ระบบยิง Webhookslip.verified มาที่เซิร์ฟเวอร์คุณแทน — เช็ค 4 ข้อเดิมจาก data.amount / data.transRef ได้เหมือนกัน

3. รูปอยู่ในรูปแบบอื่น

POST /verify รับสลิปได้ 5 แบบ — ผลลัพธ์ โควตา และแคช 24 ชม. เหมือนกันทุกแบบ (ส่งรูปเดิมซ้ำคนละแบบก็ยังได้ cached: true ไม่หักโควตาซ้ำ)

มีอยู่ในมือส่งอย่างไรContent-Type
ไฟล์รูป / Buffer / bytesส่งเป็น body ตรง ๆ — เล็กและเร็วที่สุด แนะนำapplication/octet-stream (หรือ image/jpeg · image/png)
ไฟล์จากฟอร์มอัปโหลดส่งต่อเป็น multipart ฟิลด์ไฟล์ชื่อ filemultipart/form-data
base64 หรือ data URLใส่ในฟิลด์ image ของ JSONapplication/json
URL ของรูป (S3, CDN)ใส่ในฟิลด์ url ของ JSON — เราดาวน์โหลดให้application/json
ข้อความใน QR ของสลิป (ไม่มีรูป)ใส่ในฟิลด์ qr ของ JSONapplication/json
API="https://api.slip-bolt.com/api/v1/verify"

# 1) raw bytes
curl -X POST "$API" -H "Authorization: Bearer $SLIPBOLT_API_KEY" \
  -H "Content-Type: application/octet-stream" --data-binary "@slip.jpg"

# 2) multipart/form-data — ฟิลด์ไฟล์ชื่อ file
curl -X POST "$API" -H "Authorization: Bearer $SLIPBOLT_API_KEY" \
  -F "[email protected]"

# 3) JSON base64
base64 < slip.jpg | tr -d '\n' | sed 's/.*/{"image":"&"}/' |
  curl -X POST "$API" -H "Authorization: Bearer $SLIPBOLT_API_KEY" \
    -H "Content-Type: application/json" --data-binary @-

# 4) JSON url — ลิงก์ https ของรูปที่เปิดได้จากอินเทอร์เน็ต
curl -X POST "$API" -H "Authorization: Bearer $SLIPBOLT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.example.com/slips/1001.jpg"}'

# 5) JSON qr — ข้อความที่สแกนได้จาก QR บนสลิป
curl -X POST "$API" -H "Authorization: Bearer $SLIPBOLT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"qr":"0046000600000101030140225202610075020019535816MIR5102TH9104A1B2"}'

ขนาดสูงสุด 5 MB ต่อคำขอ · JPEG หรือ PNG · ต้องเห็น QR บนสลิปครบ (รูปที่ครอปจน QR ขาดจะได้ INVALID) base64 ทำให้ข้อมูลโตขึ้นราว 33% ⇒ ส่งแบบ JSON ได้รูปใหญ่สุดประมาณ 3.7 MB — รูปใหญ่กว่านั้นใช้ raw bytes หรือ multipart

ส่งเป็น URL หรือข้อความ QR

สองแบบนี้ส่งเป็น JSON เหมือน base64 — ใส่ อย่างใดอย่างหนึ่ง ใน image / url / qr (ใส่เกินหนึ่งอย่างได้ 400)

url — เราดาวน์โหลดรูปให้ แล้วตรวจเหมือนคุณส่งไฟล์นั้นมาเอง

  • ต้องเป็น https:// ที่เปิดได้จากอินเทอร์เน็ตโดยไม่ต้องล็อกอิน (เช่น pre-signed URL ของ S3) · localhost / IP ภายในถูกปฏิเสธ
  • ไม่ตาม redirect — ใส่ลิงก์ปลายทางจริง · ไฟล์ไม่เกิน 5 MB · รอได้ไม่เกิน 10 วินาที
  • ดาวน์โหลดไม่ได้ → 400 พร้อมเหตุผล (ไม่หักโควตา)

qr — ข้อความที่อ่านได้จาก QR บนสลิป เหมาะกับแอปที่สแกน QR เองอยู่แล้ว ไม่ต้องอัปโหลดรูป

  • ส่งข้อความตามที่สแกนได้ทั้งก้อน ไม่ต้องตัดหรือแปลง
  • ข้อความเดิมส่งซ้ำใน 24 ชม. ได้ cached: true เหมือนส่งรูปเดิมซ้ำ
const API = 'https://api.slip-bolt.com/api/v1/verify';

async function verifyJson(payload) {
  const res = await fetch(API, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SLIPBOLT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(30_000),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.code ?? body.error}: ${body.message}`);
  return body.data;
}

const byUrl = await verifyJson({ url: process.env.SLIP_URL });
const byQr = await verifyJson({ qr: process.env.SLIP_QR });
console.log(byUrl.status, byQr.status);

4. ทดสอบ webhook ในเครื่อง

SlipBolt ส่ง webhook ไปได้เฉพาะ https:// ที่เป็นที่อยู่สาธารณะ จึงยิงเข้า localhost ตรง ๆ ไม่ได้ — มีสองทาง:

ทางที่ 1 — เซ็น event เองแล้วยิงเข้า localhost

เร็วที่สุด ไม่ต้องเปิดอะไรออกอินเทอร์เน็ต · สคริปต์นี้เซ็นแบบเดียวกับที่ระบบจริงเซ็นทุกประการ ใช้ secret เดียวกับที่ตั้งไว้ใน handler ของคุณ (ตัวอย่าง handler)

Terminal
SECRET="whsec_ใส่ของคุณ"
URL="http://localhost:3000/webhooks/slipbolt"

TS=$(date +%s)
BODY='{"event":"slip.verified","id":"evt_local_'$TS'","created":'$TS',"data":{"transactionId":"txn_local_1","transRef":"TEST'$TS'","amount":250,"status":"SUCCESS","bankCode":"004","bankName":"ธนาคารกสิกรไทย","sender":"นาย ทดสอบ ระบบ","receiver":"ร้านของคุณ","verifiedAt":"2026-06-17T03:21:06.412Z","latencyMs":480}}'
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')

curl -i -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-Slipbolt-Event: slip.verified" \
  -H "X-Slipbolt-Signature: t=$TS,v1=$SIG" \
  --data-binary "$BODY"

ได้ 200 = handler ตรวจ signature ผ่าน · ลองแก้ SECRET ให้ผิดแล้วยิงอีกรอบ ต้องได้ 400 (ถ้ายังได้ 200 แปลว่า handler ไม่ได้ตรวจ signature จริง)

ทางที่ 2 — เปิดอุโมงค์แล้วให้ระบบจริงยิงมา

ใช้เมื่ออยากเห็น event จากสลิปจริง — เปิดอุโมงค์ด้วยเครื่องมือที่ถนัด (cloudflared, ngrok ฯลฯ) เช่น

Terminal
cloudflared tunnel --url http://localhost:3000

เอา URL https://….trycloudflare.com ที่ได้ ต่อด้วย path ของ handler ไปใส่ที่ Settings → Webhooks แล้วกด ส่ง event ตัวอย่าง — ไม่กินโควตา

เช็กลิสต์ก่อนขึ้น production

  • เปลี่ยนคีย์เป็น sk_live_ และเก็บใน environment variable ของเซิร์ฟเวอร์ ไม่อยู่ในโค้ดหรือ repo
  • เช็คครบ 4 ข้อ ก่อนปล่อยของ และ transRef ติด UNIQUE ในฐานข้อมูล
  • เรียก GET /me ตอนบูตระบบ — ยืนยันว่าคีย์ถูกร้าน และ payeeAccounts มีบัญชีที่ลูกค้าโอนเข้า
  • จัดการ 402 (โควตาหมด) ให้แจ้งเตือนทีมคุณ ไม่ใช่เงียบ — ลูกค้าจะจ่ายเงินแล้วออเดอร์ไม่ปิด
  • ถ้าใช้ webhook: ตรวจ signature จาก raw body · ตอบ 2xx ภายใน 10 วินาที · กันประมวลผล id ซ้ำ
  • เก็บ requestId (จาก meta หรือ error) ลง log — ส่งมาให้ทีมงานแล้วเราไล่ให้ได้ทันที