บันทึกและ audit log
หน้านี้คือที่ที่คุณมาตอบคำถาม 3 ข้อ
- เรายิงอะไรไปแล้วได้อะไรกลับมา
- webhook ใบนั้นส่งถึงเราหรือยัง ล้มที่ attempt ไหน เพราะอะไร
- ใครในทีมกดอะไรในระบบหลังบ้าน
1. request / response ย้อนหลัง
GET /v1/bo/logsทุก response ของเรามี request_id — ทั้งตอนสำเร็จและตอน error
{ "error": {
"code": "signature_invalid",
"type": "authentication_error",
"message": "Signature does not match the canonical string.",
"message_th": "ลายเซ็นไม่ตรงกับ canonical string",
"retryable": false,
"retry_after_seconds": null,
"request_id": "req_01JQA5T8V0X2Z4B6D8F0H2J4L6",
"details": {} } }เก็บ request_id ทุกครั้งที่ยิง ไม่ใช่เฉพาะตอน error
เวลาส่งเคสให้ทีมงาน request_id ตัวเดียวประหยัดเวลาไปหนึ่งวัน เคสที่ส่งมาว่า "สร้างรายการไม่ได้เมื่อบ่ายวานนี้" โดยไม่มี request_id และไม่มี deposit_id ไล่หาแทบไม่ได้เลย
สิ่งที่ห้ามอยู่ใน log ฝั่งคุณ
qr_payload— ข้างในมีข้อมูลบัญชีของผู้ถอน ผูกไว้ในเงื่อนไขการใช้งานว่า ห้าม log ห้ามเก็บลงฐานข้อมูล ห้ามส่งต่อ ใช้แสดงผลแล้วทิ้ง- เลขบัญชีปลายทางแบบเต็ม
sk_live_*และค่าPG-Signature
qr_payload มีอายุเท่ากับ expires_at ซึ่งบางเส้นทางสั้นถึง ~4 นาที ⇒ log ไว้ก็ใช้อะไรไม่ได้อยู่ดี มีแต่ความเสี่ยง
2. สถานะการส่ง webhook ทุกใบ
curl -sS "https://api.example-wl.com/v1/bo/webhooks/whep_01K2A9CDEF3G/deliveries?limit=50&status=failed" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN"{
"data": [
{
"id": "whd_01K3M8QX4C7RZ2XT5NBVKD9H0P",
"object": "webhook_delivery",
"endpoint_id": "whep_01K2A9CDEF3G",
"event_id": "evt_01K3M8QW7Z4N2VYB6H0RSTA9CD",
"event_type": "deposit.settled",
"status": "failed",
"attempt": 11,
"resend": false,
"created_at": "2026-08-21T04:12:33.940Z",
"last_attempt_at": "2026-08-25T00:49:07.552Z",
"next_retry_at": null,
"response_status": 504,
"response_body_excerpt": "upstream request timeout",
"duration_ms": 15002,
"error": "response_timeout"
},
{
"id": "whd_01K3M9F3AB8SY4WT6PCXLE1J2Q",
"object": "webhook_delivery",
"endpoint_id": "whep_01K2A9CDEF3G",
"event_id": "evt_01K3M9F2XN8VQKR4T0BDHY7WZE",
"event_type": "deposit.under_review",
"status": "delivered",
"attempt": 2,
"resend": false,
"created_at": "2026-08-21T04:19:08.810Z",
"last_attempt_at": "2026-08-21T04:19:19.043Z",
"next_retry_at": null,
"response_status": 200,
"response_body_excerpt": "ok",
"duration_ms": 214,
"error": null
}
],
"has_more": false,
"next_cursor": null
}สถานะของ delivery มี 4 ค่า ไม่มีค่าอื่น
status | แปลว่า |
|---|---|
pending | ยังอยู่ในรอบ retry · ดูเวลาถัดไปที่ next_retry_at |
delivered | ได้ 2xx แล้ว |
failed | ครบ 11 attempt แล้วยังไม่ได้ 2xx · event ยังอยู่ 30 วัน กดส่งซ้ำได้ |
canceled | endpoint ถูกลบ/ปิด ระหว่างที่ delivery ยังค้าง |
อะไรนับว่าสำเร็จ
| เกณฑ์ | ค่า |
|---|---|
| สำเร็จ | 200 201 202 204 เท่านั้น |
| timeout เชื่อมต่อ | 5 วินาที |
| timeout ถึง byte แรกของ response | 10 วินาที |
| timeout รวมทั้ง request | 15 วินาที (แนะนำให้ตอบภายใน 3 วินาที) |
| body ที่เราอ่านเก็บไว้ | สูงสุด 8 KB · เนื้อหาไม่มีผลต่อความสำเร็จ |
| ขนาด body ที่เราส่ง | ไม่เกิน 256 KB (รับประกัน) |
3xx | 🔴 ไม่ตาม redirect = ล้มเหลว |
410 Gone | 🔴 ล้มเหลวถาวร หยุด retry ทันที + ตั้งธง endpoint หายไป |
429 | ล้มเหลว → retry ตามตาราง (เรา ไม่อ่าน Retry-After ของคุณ ตารางคงที่เพื่อให้คาดเดาได้) |
4xx/5xx อื่น · TLS/DNS/connection error | ล้มเหลว → retry ตามตาราง |
ตอบ 2xx ก่อน แล้วค่อยทำงาน
งานหนักใน handler ⇒ ตอบช้าเกิน 15 วินาที ⇒ เรานับว่าล้มเหลว ⇒ retry ⇒ ประมวลผลซ้ำ ⇒ เครดิตซ้ำ รูปแบบที่ถูก: verify ลายเซ็น → เขียนลงคิว → ตอบ 200 → ทำงานจริงแบบ async
ตารางรอบ retry
| attempt | หน่วงจากครั้งก่อน | สะสมจากที่สร้าง event |
|---|---|---|
| 1 | ทันที | 00:00:00 |
| 2 | 10 วินาที | 00:00:10 |
| 3 | 1 นาที | 00:01:10 |
| 4 | 5 นาที | 00:06:10 |
| 5 | 30 นาที | 00:36:10 |
| 6 | 2 ชั่วโมง | 02:36:10 |
| 7 | 6 ชั่วโมง | 08:36:10 |
| 8 | 12 ชั่วโมง | 20:36:10 |
| 9 | 24 ชั่วโมง | 44:36:10 |
| 10 | 24 ชั่วโมง | 68:36:10 |
| 11 | 24 ชั่วโมง | 92:36:10 |
รวม 11 ครั้ง หน้าต่างประมาณ 92 ชั่วโมง 36 นาที (3 วัน 20 ชั่วโมง) · มี jitter ±10% ต่อรอบ ⇒ ค่าจริงดูที่ next_retry_at
หน้าต่าง retry สั้นกว่าอายุ Idempotency-Key โดยตั้งใจ
retry รอบสุดท้าย (~92 ชั่วโมง) ยังชนกับ idempotency record ของคุณที่มีอายุ 7 วันได้เสมอ ⇒ 🔴 TTL ของ idempotency ฝั่งคุณ ห้ามสั้นกว่า 7 วัน ไม่งั้นรายการซ้ำจะโผล่มาจาก retry รอบท้าย ๆ
เลิก retry เมื่อไร
- ได้
2xx - ครบ 11 attempt →
failed(event ยังอยู่ 30 วัน กดส่งซ้ำได้) - ได้
410 - คุณลบหรือปิด endpoint (delivery ที่ค้างกลายเป็น
canceled) - ระบบปิด endpoint ให้อัตโนมัติ
3. กดส่งซ้ำ
ส่งซ้ำใบเดียว
curl -sS -X POST "https://api.example-wl.com/v1/bo/webhooks/whep_01K2A9CDEF3G/deliveries/whd_01K3M8QX4C7RZ2XT5NBVKD9H0P/resend" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Idempotency-Key: resend-whd01K3M8QX4C7RZ2XT5NBVKD9H0P-01" \
-H "Content-Type: application/json"{
"delivery_id": "whd_01K3V2R8T5WYZ7XB1KDMNQH4PS",
"event_id": "evt_01K3M8QW7Z4N2VYB6H0RSTA9CD",
"status": "pending",
"attempt": 1,
"resend": true,
"created_at": "2026-08-23T05:00:00.412Z"
}| เรื่อง | สิ่งที่เกิด |
|---|---|
event.id | 🔴 เหมือนเดิมทุกประการ ⇒ การกันซ้ำฝั่งคุณยังทำงาน กดส่งซ้ำจึงปลอดภัยเสมอ |
| ไบต์ของ body | เหมือนเดิมทุกไบต์ — payload ถูกเก็บไว้ตั้งแต่ตอนสร้าง ไม่ render ใหม่ ⇒ ไม่มีปัญหาเวอร์ชัน API เปลี่ยนกลางทาง |
| ลายเซ็น | คำนวณใหม่ (เพราะ t ต้องใหม่) เซ็นด้วยกุญแจที่ active ตอนนี้ |
| delivery metadata | อยู่ใน header เท่านั้น: X-PG-Delivery-Id ใบใหม่ · X-PG-Attempt: 1 · X-PG-Resend: manual |
| รอบ retry | ได้ครบ 11 รอบใหม่ |
| อายุ | ส่งซ้ำได้ภายใน 30 วัน นับจาก created_at ของ event |
กดส่งซ้ำแล้วเครดิตซ้ำไหม — ไม่ ถ้าคุณทำ 2 อย่างนี้ครบ
- transactional inbox มี
UNIQUE(event_id)เก็บอย่างน้อย 31 วัน · เจอซ้ำ ตอบ200อย่าตอบ error - version guard เป็น
UPDATE ... WHERE version < $newแล้วเช็คจำนวนแถวที่ถูกแก้ · ได้ 0 แถว = event เก่า ⇒ ตอบ200แล้วไม่ทำอะไร
ระบบรับประกันว่า ไม่มีสอง event ของ object เดียวกันที่ version เท่ากัน ⇒ รูปแบบนี้ปลอดภัยจริง
ส่งซ้ำทีละมาก
curl -sS -X POST "https://api.example-wl.com/v1/bo/webhooks/whep_01K2A9CDEF3G/replay" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Idempotency-Key: replay-whep01K2A9CDEF3G-20260823-01" \
-H "Content-Type: application/json" \
-d '{
"event_types": ["deposit.settled", "deposit.reversed"],
"created_after": "2026-08-18T00:00:00.000Z",
"created_before": "2026-08-21T00:00:00.000Z",
"only_failed": true
}'ทุก /v1/bo/* ใช้ OIDC access token ของเจ้าหน้าที่ ไม่ใช้ sk_live_* หรือ PG HMAC การอ่าน delivery ใช้ token อย่างเดียว ส่วน resend/replay เป็น privileged action จึงต้องมี step-up token ที่ยังไม่หมดอายุด้วย
- สูงสุด 10,000 event ต่อคำสั่ง ปล่อยที่ 50 ใบ/วินาที
- มีบันทึก audit ทุกครั้ง
- 🔴 คำสั่งแบบนี้ ไม่นับ ในโควตา
webhook_resend(โควตานั้นนับ "คำสั่ง" ที่ 10 ครั้ง/นาที ต่อ endpoint)
ก่อนกด replay ให้ยืนยันว่า handler ของคุณกันซ้ำได้จริงก่อน
replay 10,000 ใบเข้า handler ที่ไม่มี UNIQUE(event_id) = เครดิตซ้ำ 10,000 ครั้ง ทดสอบด้วยการ resend ใบเดียวก่อน แล้วดูว่ายอดในระบบคุณ ไม่ขยับ จึงค่อยยิงทั้งชุด
endpoint ล่มยาว
ล่มติดกัน 6 ชั่วโมง → อีเมลเตือน + แถบแจ้งเตือน (ยัง retry ปกติ)
ล่มติดกัน 12 ชั่วโมง → อีเมลเตือนครั้งที่ 2 + แจ้งผู้ให้บริการชั้นบน
ล่มติดกัน 24 ชั่วโมง → 🔴 ระบบปิด endpoint ให้อัตโนมัติ
เงื่อนไข: 24 ชั่วโมงล่าสุดสำเร็จ 0 ครั้ง **และ** delivery ที่ล้มเหลว ≥ 50 ใบ
ผล: หยุดสร้าง delivery ใหม่ (event ยังเก็บครบ 30 วัน)
· delivery ที่ค้างกลายเป็น canceled
· ยิง webhook.endpoint.disabled ไป endpoint กลุ่มอื่น + อีเมล + บันทึก audit- เปิดคืนต้องกดเอง จากระบบหลังบ้าน — มีปุ่มส่งซ้ำ delivery ที่ล้มเหลวใน 72 ชั่วโมงล่าสุดให้
- endpoint ที่มี success อย่างน้อย 1 ครั้งใน 24 ชั่วโมง จะไม่ถูกปิด ⇒ endpoint ที่ล้ม 90% ยังอยู่ต่อได้ แต่จะโดนเตือน
- 🔴 ถ้าคุณมี endpoint เดียวและมันถูกปิด event ที่ปิดไม่ได้จะถูกส่งทางอีเมล ไปยังผู้ติดต่อทางเทคนิค
นี่คือเหตุผลที่ผู้ติดต่อทางเทคนิคต้องเป็นอีเมลที่มีคนอ่าน
ไม่ใช่ noreply@ และไม่ใช่กล่องที่ไม่มีใครเปิด วันที่ระบบส่งอีเมลว่า endpoint ถูกปิด คือวันที่คุณกำลังจะไม่รู้เรื่องการกลับรายการที่เกิดขึ้นกับกระเป๋าตัวเอง
header ที่ติดมากับทุก delivery
POST /webhooks/pg HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: PG-Webhooks/1.0
X-PG-Signature: v1,kid=whk_2026a,t=1755749553,s=3rPYqVQ
X-PG-Event-Id: evt_01K3M8QW7Z4N2VYB6H0RSTA9CD
X-PG-Event-Type: deposit.settled
X-PG-Delivery-Id: whd_01K3M8QX4C7RZ2XT5NBVKD9H0P
X-PG-Attempt: 3
X-PG-Api-Version: 2026-08-01
X-PG-Livemode: truedelivery headers ใช้เพื่อ observability เท่านั้น
verify ลายเซ็นบน raw bytes ก่อน แตะ body · business event อยู่ใน body; delivery headers ไม่ได้อยู่ใน canonical signature จึงห้ามใช้ตัดสินเรื่องเงิน สิทธิ์ หรือ state X-PG-Signature ต้อง parse แบบแยก key=value ห้าม parse ด้วยตำแหน่ง เพราะเราอาจเพิ่ม field ในอนาคต
สิ่งที่ควร log ฝั่งคุณทุกครั้ง: event.id · X-PG-Delivery-Id · X-PG-Attempt · ผลของ version guard (กี่แถวถูกแก้) · เวลาที่ใช้ใน handler
ตามหา event ที่หายไป
พบช่องว่างของ sequence จาก สคริปต์กระทบยอด แล้วทำตามลำดับนี้
- ช่องว่างที่เกิดภายใน 4 วันล่าสุด = ปกติ event ยังอยู่ในรอบ retry
- ดึง event ตรง ๆ ด้วย
GET /v1/pay/events?since_sequence=— ถ้าดึงได้แปลว่าไม่ได้หาย แค่ยังส่งไม่ถึง - ดู
GET /v1/bo/webhooks/{endpoint_id}/deliveriesว่าใบนั้นstatusเป็นอะไร ล้มที่response_statusไหน - แก้ที่ต้นเหตุก่อน (endpoint ตอบ
3xx· TLS หมดอายุ · handler timeout) แล้วค่อย กดส่งซ้ำ - ช่องว่างที่ค้างเกิน 4 วันและดึงจาก
/v1/pay/eventsไม่ได้ = หายจริง ⇒ ส่งเรื่องให้ทีมงานพร้อมช่วงsequence
4. ใครกดอะไรในระบบหลังบ้าน
GET /v1/bo/reports/action-logsทุกการกระทำที่มีผลกับเงินหรือกับการตั้งค่าถูกบันทึกไว้ — การอนุมัติและปฏิเสธรายการถอน · การต่อเวลา · การเปิดเรื่องร้องเรียน · การกดส่งซ้ำ webhook · การแก้ค่าตั้งค่า
สิ่งที่ audit log ฝั่งเรา ไม่มี ให้
บันทึกภายในของเจ้าหน้าที่ที่ตัดสินเรื่องในคิวตรวจสอบ ชื่อเจ้าหน้าที่ และเวลาที่ผูกพันภายใน ไม่ถูกส่งออก ทุกช่องทาง สิ่งที่คุณเห็นเกี่ยวกับคิวตรวจสอบมี 3 อย่าง: review.escalated · review.four_eyes_required · review.estimated_resolution_bucket
เช็กลิสต์งาน log
- [ ] log
event.id+X-PG-Delivery-Id+X-PG-Attemptทุกครั้งที่รับ webhook - [ ] transactional inbox มี
UNIQUE(event_id)เก็บอย่างน้อย 31 วัน · ซ้ำ = ตอบ200 - [ ] เก็บ
request_idของทุก request ที่ยิงออก ไม่ใช่เฉพาะตอน error - [ ] ไม่ log
qr_payloadไม่ log เลขบัญชีปลายทางแบบเต็ม - [ ] alert เมื่อ handler ตอบไม่ใช่ 2xx เกิน 1% ใน 5 นาที
- [ ] alert เมื่อตรวจลายเซ็นไม่ผ่านแม้แต่ครั้งเดียว
- [ ] ทดสอบ resend ใบเดียวแล้วยืนยันว่ายอดไม่ขยับ ก่อนใช้ replay ทีละมาก
- [ ] ผู้ติดต่อทางเทคนิคเป็นอีเมลที่มีคนอ่านจริง
ขั้นต่อไป
- การส่งซ้ำและ endpoint ล่ม — รายละเอียดฝั่ง webhook เต็ม ๆ
- ลำดับไม่การันตี — version guard — ทำไม resend ถึงปลอดภัย
- ตรวจลายเซ็น Ed25519 — verify บน raw bytes
- รายงานและการกระทบยอด — ช่องว่างของ
sequenceมาจากที่นี่ - ตั้งค่า — ข้อกำหนดของ endpoint และ event ที่ปิดไม่ได้
