ภาพรวม

AUTO P2P จับคู่คนฝากกับคนถอนให้โอนเงินหากันตรงผ่านบัญชีธนาคาร AUTO P2P ไม่ถือเงิน ไม่มีกระเป๋าเงิน และไม่คิดค่าธรรมเนียมแทนคุณ การกันเงิน ค่าธรรมเนียม และการลงบัญชีอยู่ฝั่งระบบของคุณทั้งหมด

เซิร์ฟเวอร์https://autop2p.tech (ตอนนี้เป็นระบบทดสอบ ห้ามใช้เงินจริง)
Base path/api/p2p/provider
รูปแบบJSON ทั้งขาเข้าและขาออก, THB เท่านั้น
สัญญาcontractVersion: 1
  • ถอน (payout): กันเงินของร้านก่อน แล้วส่งคำสั่งถอน → AUTO P2P จับคู่กับคนฝาก → ผู้รับยืนยันว่าได้เงินครบ → callback COMPLETED ให้ตัดเงินที่กันไว้
  • ฝาก (payin): ส่งคำสั่งฝาก → ได้ยอดที่ต้องโอนและหน้า payUrl ให้คนฝาก → คนฝากโอน → callback COMPLETED ให้เครดิตร้าน

เริ่มใช้งานเร็ว

  1. ขอบัญชีร้านจากทีม AUTO P2P แล้วรับคำเชิญ ตั้งรหัสผ่านและแอปยืนยันตัวตนที่ /partner
  2. เจ้าของร้านกด "สร้างคีย์" ได้ apiSecret และ callbackSecret (แสดงครั้งเดียว) เก็บเป็นตัวแปรลับบนเซิร์ฟเวอร์
  3. จดค่า Account ID (header x-account-id) และ gatewayAccountRef จากกล่อง "ข้อมูลเชื่อมต่อ"
  4. เรียก GET /api/p2p/provider/health ให้ได้ 200 (ดูการเซ็นด้านล่าง)
  5. ทำตัวรับ callback แล้วตั้ง Callback URL ในพอร์ทัล — ก่อนตั้ง API จะไม่รับคำสั่งจริง (409 CALLBACK_URL_REQUIRED)
  6. ทดสอบตามลำดับทดสอบ
ห้ามส่งคีย์ทางแชท อีเมล GitHub หรือใส่ในโค้ดที่ commit ทีม AUTO P2P ไม่ต้องใช้คีย์ของคุณ ถ้าคีย์รั่ว เจ้าของร้านสร้างคีย์ใหม่ได้ทันที (คีย์เดิมใช้ไม่ได้ทันที)

การเซ็นคำขอ

Headerค่า
x-account-idAccount ID ของร้าน
x-timestampเวลาปัจจุบันเป็นมิลลิวินาที 13 หลัก (ต่างจากเซิร์ฟเวอร์ได้ไม่เกิน ±5 นาที)
x-signatureHMAC-SHA256 เป็น hex ตัวเล็ก 64 ตัว
Content-Typeapplication/json
ข้อความที่เซ็น
timestamp + METHOD + path + body
  METHOD = POST | GET (uppercase)
  path   = full path incl. /api and any query, e.g. /api/p2p/provider/payout
  body   = "" for GET or an empty body, otherwise JSON.stringify(body)
signature = hex( HMAC_SHA256(apiSecret, string to sign) )

ลายเซ็นผิดส่วนใหญ่มาจาก 3 ข้อนี้

  1. สร้างสตริง JSON ครั้งเดียว แล้วใช้สตริงเดียวกันทั้งเซ็นและส่ง
  2. ส่งยอดเงินเป็น string เช่น "1000.50" — เซิร์ฟเวอร์ตรวจกับ JSON.stringify ของ body ที่ parse แล้ว ตัวเลข 1000.50 จะกลายเป็น 1000.5 ลายเซ็นไม่ตรง; ห้ามมีช่องว่าง; PHP ต้องใช้ JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
  3. อย่าส่ง field ที่ไม่มีในตาราง (400 Unknown request field)
const crypto = require('node:crypto');
const BASE = 'https://autop2p.tech';
const ACCOUNT_ID = process.env.P2P_ACCOUNT_ID;   // x-account-id
const API_SECRET = process.env.P2P_API_SECRET;   // from the partner portal

async function p2p(method, path, body) {
  const raw = body === undefined ? '' : JSON.stringify(body); // build once, sign and send the same string
  const ts = String(Date.now());
  const signature = crypto.createHmac('sha256', API_SECRET)
    .update(ts + method.toUpperCase() + path + raw).digest('hex');
  const res = await fetch(BASE + path, {
    method, body: raw || undefined, redirect: 'error', signal: AbortSignal.timeout(15000),
    headers: { 'content-type': 'application/json', 'x-account-id': ACCOUNT_ID, 'x-timestamp': ts, 'x-signature': signature },
  });
  const text = await res.text();
  let json = null; try { json = text ? JSON.parse(text) : null; } catch {}
  return { status: res.status, body: json };
}

// await p2p('GET', '/api/p2p/provider/health');  // 200 = connected

ลายเซ็นไม่ตรง? ลองในตัวช่วยคำนวณลายเซ็น

รายการ endpoint

ทุก path อยู่ใต้ /api/p2p/provider และต้องเซ็นทุกคำขอ

MethodPathใช้ทำอะไร
POST/payoutสร้างคำสั่งถอน (หลังกันเงินแล้ว)
POST/payinสร้างคำสั่งฝาก ได้ยอดโอนและบัญชีปลายทาง
GET/requests/{PAYIN|PAYOUT}/{referenceId}ตามผลด้วยเลขของคุณ (ใช้ตอนไม่ได้คำตอบ)
GET/status/{ref}สถานะปัจจุบันด้วยเลข P2PW-… / P2PM-…
GET/payout/{referenceId}/receive-linkลิงก์หน้าผู้รับเงิน
POST/payout/{ref}/cancelยกเลิกถอนที่ยังไม่เคยจับคู่
GET/events?cursor={eventId}ดึงผลสุดท้ายทั้งหมดเพื่อกระทบยอด (ทีละ 100)
GET/healthเช็กการเชื่อมต่อ และระบบรับงานใหม่อยู่ไหม

จำกัดความถี่ต่อ IP: 100/วินาที, 1,000/10 วินาที, 5,000/นาที เกินได้ 429

สร้างคำสั่งถอน

POST/api/p2p/provider/payout
กันเงินของร้าน (รวมค่าธรรมเนียมของคุณ) และบันทึกคำสั่งไว้ก่อน แล้วจึงเรียก endpoint นี้
Fieldจำเป็นคำอธิบาย
referenceId✓เลขคำสั่งของคุณ ไม่ซ้ำ A-Z a-z 0-9 _ . : - ยาว 1–128
holdReference✓ต้องเท่ากับ referenceId (ยืนยันว่ากันเงินแล้ว)
amount✓string เช่น "1000.00" ทศนิยมไม่เกิน 2
currency"THB"
gatewayAccountRef✓ค่าจากพอร์ทัลร้าน
sourceMerchantRef✓รหัสร้านย่อยในระบบคุณ (≤128)
bankCode✓KBANK, SCB, BBL, KTB, BAY, TTB, GSB …
accountNumber✓เลขบัญชีผู้รับ ตัวเลข 6–20 หลัก
accountName✓ชื่อบัญชีผู้รับ
promptpayIdพร้อมเพย์ผู้รับ (มือถือ 10 หลัก หรือบัตร 13 หลัก) ใช้ทำ QR
sourcePartnerRefรหัสอ้างอิงเพิ่มเติม
callbackUrlถ้าส่ง ต้องตรงกับที่ตั้งในพอร์ทัล
201
{
  "ref": "P2PW-9ef83b1bdb15d0fc52c4592d6b931e68",
  "expiresAt": "2026-09-29T10:15:00.000Z",
  "receiveUrl": "https://autop2p.tech/receive/<token>",
  "lane": "SMALL",
  "acceptance": "ACCEPTED",
  "contractVersion": 1
}
  • เก็บ ref (P2PW-…) คู่กับคำสั่งของคุณ
  • receiveUrl เป็นหน้าให้ผู้รับติดตามและกด "ได้รับยอดนี้แล้ว" ส่งให้เจ้าของบัญชีเท่านั้น
  • อาจได้เงินหลายครั้งจากคนฝากหลายคน callback ส่งเฉพาะตอนจบ ระหว่างทางใช้ GET /status/{ref}

สร้างคำสั่งฝาก

POST/api/p2p/provider/payin
Fieldจำเป็นคำอธิบาย
referenceId✓เลขคำสั่งของคุณ (กติกาเดียวกับ payout)
amount✓string เช่น "500.00"
currency"THB"
gatewayAccountRef✓ค่าจากพอร์ทัลร้าน
sourceMerchantRef✓รหัสร้านย่อยในระบบคุณ
sourcePartnerRefรหัสอ้างอิงเพิ่มเติม
dryRuntrue = เช็กว่ามีคู่ไหม ไม่สร้างรายการ
callbackUrlถ้าส่ง ต้องตรงกับที่ตั้งในพอร์ทัล
201 — จับคู่ได้
{
  "ref": "P2PM-7d7c576c9c2fce92f618a45352b5acb2",
  "transferAmount": 500.37,
  "payUrl": "https://autop2p.tech/pay/P2PM-7d7c576c9c2fce92f618a45352b5acb2",
  "expiresAt": "2026-09-29T10:15:00.000Z",
  "matched": true,
  "acceptance": "ACCEPTED",
  "contractVersion": 1
}
  • พาคนฝากไปที่ payUrl (มีบัญชีปลายทาง ยอดที่ต้องโอน QR เวลาที่เหลือ แนบสลิป ไทย/อังกฤษ)
  • transferAmount = ยอดฝาก + เศษสตางค์ 1–99 ที่ใช้แยกรายการ ต้องโอนตรงถึงสตางค์ เศษนี้ไม่ใช่ค่าธรรมเนียม ยอดที่ลงบัญชีคือ amount
  • ACCEPTED ยังไม่ใช่เงินเข้า อย่าเครดิตร้านจนได้ callback COMPLETED
201 — ไม่มีคู่ (คำสั่งจบแล้ว ใช้ช่องทางอื่นได้)
{ "matched": false, "acceptance": "REJECTED", "reason": "no liquidity in withdraw queue", "contractVersion": 1 }

เมื่อไม่ได้คำตอบ (สำคัญที่สุดเรื่องเงิน)

timeout, เน็ตหลุด, 5xx หรือ body อ่านไม่ออก = "ยังไม่ทราบผล" (UNKNOWN) ไม่ใช่ล้มเหลว ห้ามสร้างคำสั่งใหม่ด้วยเลขใหม่ ห้ามคืนเงินที่กันไว้ ห้ามส่งไปช่องทางอื่น
GET/api/p2p/provider/requests/{PAYIN|PAYOUT}/{referenceId}
ผลทำต่อ
200ได้ผลที่บันทึกไว้ ใช้ result เหมือนได้คำตอบปกติ
404คำสั่งยังไม่ถึงเรา → ส่งคำสั่งเดิม referenceId เดิม body เดิมทุกตัวอักษรซ้ำได้
อื่นๆยังไม่ทราบผล ลองถามใหม่ภายหลัง

ส่ง referenceId เดิมพร้อมข้อมูลเดิม = ได้ผลเดิม ไม่สร้างซ้ำ ส่ง referenceId เดิมแต่ข้อมูลต่าง = 409

สถานะและ endpoint อื่น

GET/api/p2p/provider/status/{ref}
200
{
  "status": "PENDING",
  "engineStatus": "QUEUED",
  "requestId": "WD-20260929-0001",
  "remaining": "600",
  "completedAmount": "400",
  "expiresAt": "2026-09-29T10:15:00.000Z",
  "terminalEvent": null
}

status: PENDING / COMPLETED / FAILED / EXPIRED ถ้าจบแล้ว terminalEvent คือ callback เดิม ใช้แทน callback ที่หายได้

  • POST /payout/{ref}/cancel — ได้เฉพาะคำสั่งที่ยังไม่เคยจับคู่ ถ้าเคยจับคู่ได้ 409 เมื่อยกเลิกสำเร็จจะได้ callback PAYOUT FAILED ให้คืนเงินที่กันไว้
  • GET /events → ?cursor=<nextCursor> จน nextCursor เป็น null ใช้กระทบยอด

Error ที่พบบ่อย

HTTPข้อความสาเหตุ / แก้
401Invalid integration authenticationheader ขาด, timestamp ไม่ใช่ 13 หลักหรือเกิน ±5 นาที, ลายเซ็นไม่ใช่ hex 64 ตัว
401Invalid signatureสตริงที่เซ็นไม่ตรง ดูกฎ 3 ข้อ
401Integration unavailableAccount ID ผิด หรือร้านถูกระงับ
400Unknown request fieldส่ง field ที่ไม่มีในตาราง
400Invalid referenceIdตัวอักษรไม่อนุญาต หรือยาวเกิน 128
400Gateway account does not match integrationgatewayAccountRef ไม่ตรง
400Callback destination must match the registered integrationcallbackUrl ไม่ตรงกับที่ตั้งในพอร์ทัล
409CALLBACK_URL_REQUIREDยังไม่ได้ตั้ง Callback URL ในพอร์ทัล
409Request ID was already used with different detailsreferenceId ซ้ำแต่ข้อมูลต่าง
404Request outcome is not known; absence is not cancellationlookup: เรายังไม่ได้รับคำสั่งนั้น
429—ส่งถี่เกินไป รอแล้วลองใหม่

400 ที่ได้หลังกันเงินแล้ว ให้ถือเป็น UNKNOWN คงเงินที่กันไว้และตรวจสาเหตุ

Callback: รูปแบบข้อมูล

เมื่อคำสั่งจบ AUTO P2P ส่ง POST ไปที่ Callback URL ของร้าน พร้อม header x-p2p-signature

PAYIN COMPLETED
{
  "event": "p2p.match", "type": "PAYIN",
  "eventId": "6f1c2d9e-8a41-4a3b-9c0e-2b7d5e1f0a11", "contractVersion": 1,
  "client_id": "<ACCOUNT_ID>", "gateway_account_ref": "<GATEWAY_ACCOUNT_REF>",
  "ref": "P2PM-7d7c576c9c2fce92f618a45352b5acb2", "reference_id": "DP-20260929-0001",
  "status": "COMPLETED", "amount": "500.00", "transfer_amount": "500.37",
  "slip_ref": null, "withdraw_ref": "P2PW-9ef83b1bdb15d0fc52c4592d6b931e68",
  "currency": "THB", "occurredAt": "2026-09-29T10:05:12.345Z"
}
PAYOUT COMPLETED
{
  "event": "p2p.withdraw", "type": "PAYOUT",
  "eventId": "0b9e7a52-3d1f-4c8e-a6b2-91f4c7d8e203", "contractVersion": 1,
  "client_id": "<ACCOUNT_ID>", "gateway_account_ref": "<GATEWAY_ACCOUNT_REF>",
  "ref": "P2PW-9ef83b1bdb15d0fc52c4592d6b931e68", "reference_id": "WD-20260929-0001",
  "status": "COMPLETED", "amount": "1000.00", "completed_amount": "1000.00",
  "hold_reference": "WD-20260929-0001", "currency": "THB", "occurredAt": "2026-09-29T10:20:44.012Z"
}
typestatusต้องทำ
PAYINCOMPLETEDเครดิตร้าน amount ครั้งเดียวต่อ reference_id
PAYINEXPIREDไม่เครดิต — ถ้าภายหลังได้ COMPLETED ของรายการเดิม ให้เครดิตตอนนั้นครั้งเดียว
PAYINFAILEDไม่เครดิต (ยืนยันแล้วว่าไม่มีเงินเข้า)
PAYOUTCOMPLETEDปิดเงินที่กันไว้เป็นจ่ายแล้ว ยอด completed_amount ครั้งเดียว
PAYOUTFAILEDยกเลิก/หมดเวลาโดยไม่มีการจ่าย (completed_amount "0.00") คืนเงินที่กันไว้ครั้งเดียว

Callback: ตรวจลายเซ็น

  1. อ่าน raw body ก่อน parse — ห้าม parse แล้ว stringify ใหม่ เพราะลำดับ key อาจต่าง
  2. HMAC_SHA256(callbackSecret, raw body) เป็น hex เทียบกับ x-p2p-signature แบบ timing-safe ไม่ตรง → 401
  3. ตรวจ contractVersion = 1, client_id, gateway_account_ref, currency, reference_id, ref, amount ตรงกับคำสั่งของคุณ ไม่ตรง → 4xx
  4. ใน transaction เดียว: บันทึก eventId (unique) + เปลี่ยนสถานะ + ลงบัญชี แล้วค่อยตอบ
// Express: read the RAW body; do not use express.json() before verifying
app.post('/p2p/callback', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected = crypto.createHmac('sha256', process.env.P2P_CALLBACK_SECRET).update(req.body).digest('hex');
  const got = String(req.get('x-p2p-signature') || '');
  if (got.length !== 64 || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected)))
    return res.status(401).end();
  const e = JSON.parse(req.body.toString('utf8'));
  // check contractVersion / client_id / gateway_account_ref / reference_id / ref / amount
  const status = await db.transaction(async (tx) => {
    const r = await tx.query('INSERT INTO p2p_callback_receipts (event_id, payload) VALUES ($1,$2) ON CONFLICT DO NOTHING', [e.eventId, e]);
    if (r.rowCount === 0) return 'duplicate';          // already handled: no second money movement
    await applyTerminalResult(tx, e);                   // status + ledger, once per order
    return 'processed';
  });
  res.status(200).json({ eventId: e.eventId, status });
});

Callback: ตอบกลับและการส่งซ้ำ

ตอบ HTTP 200 แบบนี้เท่านั้น
{ "eventId": "6f1c2d9e-8a41-4a3b-9c0e-2b7d5e1f0a11", "status": "processed" }
คำตอบของคุณAUTO P2P ทำ
2xx + eventId ตรง + processed หรือ duplicateจบ
2xx แต่ body ผิด (เช่น {"success":true})ส่ง event เดิมซ้ำ
4xx/5xx, เกิน 8 วินาที, ต่อไม่ได้, redirectส่งซ้ำ เว้นระยะ ~2, 4, 8 … วินาที สูงสุด ~4 นาทีต่อครั้ง
429รอ 15 วินาทีแล้วส่งใหม่ ไม่นับครั้ง
ล้มครบ 20 ครั้งหยุดส่ง ให้เจ้าหน้าที่ดูแล
event เดิมอาจมาซ้ำแม้คุณบันทึกแล้ว เจอ eventId ที่เคยรับ → ตอบ duplicate โดยไม่ลงเงินซ้ำ

ตัวช่วยคำนวณลายเซ็น

ใส่ค่าเดียวกับที่ระบบคุณใช้ แล้วเทียบข้อความที่เซ็นกับลายเซ็น ถ้า body ไม่ตรงกับที่เซิร์ฟเวอร์จะเห็น ระบบจะบอกให้

คำนวณในเบราว์เซอร์นี้เท่านั้น ไม่ส่งและไม่เก็บค่าใดๆ แนะนำใช้คีย์ของ sandbox หรือคีย์ทดสอบ แล้วปิดหน้านี้เมื่อเสร็จ

ลำดับทดสอบ

  1. GET /health → 200
  2. payin แบบ dryRun: true → 201 (ลายเซ็นผ่าน)
  3. ตั้ง Callback URL ในพอร์ทัล
  4. POST /payout "1000.00" → เก็บ ref และ receiveUrl
  5. POST /payin "1000.00" → ได้ payUrl และ transferAmount
  6. เปิด payUrl กด "ฉันโอนเงินแล้ว" (sandbox ไม่ต้องโอนจริง) แล้วเปิด receiveUrl กด "ได้รับยอดนี้แล้ว"
  7. ต้องได้ callback PAYIN COMPLETED และ PAYOUT COMPLETED ลงบัญชีครั้งเดียว
  8. ตัดการเชื่อมต่อก่อนได้คำตอบ /payout → ใช้ /requests/PAYOUT/{referenceId} ต้องไม่เกิดคำสั่งซ้ำ
  9. ดูรายการทั้งหมดในเมนู "รายการของร้าน" ของพอร์ทัล
autop2p.tech ตอนนี้เป็นระบบทดสอบ และยังไม่ส่ง callback ไปเซิร์ฟเวอร์ภายนอก ทีม AUTO P2P จะเปิดเมื่อพร้อมทดสอบร่วมกัน

เวอร์ชัน

วันที่เปลี่ยนแปลง
2026-09-29contractVersion 1: ร้านตั้ง Callback URL และสร้างคีย์เองในพอร์ทัล; คำสั่งจริงต้องมี Callback URL (409 CALLBACK_URL_REQUIRED)