คู่มือ step-by-step ตั้งค่า Webhook ใน Frappe v14-v16 ส่งข้อมูลอัตโนมัติเมื่อมี event เช่น สร้าง Sales Invoice, อัพเดท Stock ไปยัง Line OA, Shopee, Payment Gateway

ถ้าคุณต้องการให้ ERPNext ส่งข้อมูลไปยังระบบภายนอกอัตโนมัติ เช่น แจ้งเตือนผ่าน Line OA เมื่อสร้าง Sales Invoice หรืออัพเดทสต็อกไป Shopee เมื่อมีการรับสินค้า — Webhook คือคำตอบ บทความนี้จะสอนตั้งค่า Webhook ใน Frappe แบบละเอียดทุกขั้นตอน
Webhook คือ HTTP callback — เมื่อเกิด event ในระบบ (เช่น save document, submit invoice) Frappe จะส่ง HTTP request ไปยัง URL ที่กำหนดพร้อมข้อมูลของ document นั้น
| วิธี | ข้อดี | ข้อเสีย |
|---|---|---|
| Webhook (Push) | Real-time, ประหยัด resource | ต้องมี endpoint รับข้อมูล |
| API Polling (Pull) | ง่าย, ไม่ต้องมี server | ช้า, สิ้นเปลือง bandwidth |
ไปที่ /app/webhook/new หรือค้นหา "Webhook" จาก Awesomebar:
| Field | คำอธิบาย | ตัวอย่าง |
|---|---|---|
| DocType | Document ที่ต้องการ track | Sales Invoice |
| Document Event | Event ที่ trigger webhook | on_submit |
| Request URL | URL ปลายทาง | https://api.example.com/webhook |
| Request Method | HTTP method | POST |
| Enabled | เปิด/ปิด webhook | ✅ |
Frappe รองรับ event เหล่านี้:
| Event | เมื่อไหร่ | Use Case |
|---|---|---|
after_insert | หลังสร้าง document ใหม่ | แจ้งเตือน Line เมื่อมี Lead ใหม่ |
on_update | หลังบันทึก (save) | Sync ข้อมูลลูกค้าไป CRM |
on_submit | หลัง submit document | ส่ง invoice ไป accounting system |
on_cancel | หลัง cancel document | ยกเลิก order ใน Shopee |
on_trash | หลังลบ document | Cleanup ข้อมูลในระบบภายนอก |
on_update_after_submit | หลังแก้ไข submitted doc | อัพเดทสถานะ delivery |
on_change | เมื่อ field เปลี่ยน | Track status changes |
ถ้าไม่ต้องการให้ทุก event trigger webhook สามารถใส่ condition ได้:
# ส่งเฉพาะ invoice ที่มูลค่ามากกว่า 10,000 บาท
doc.grand_total > 10000
# ส่งเฉพาะ invoice ของลูกค้ากลุ่ม VIP
doc.customer_group == "VIP"
# ส่งเฉพาะเมื่อ status เปลี่ยนเป็น Paid
doc.status == "Paid"Frappe รองรับ 2 รูปแบบ:
เลือก field ที่ต้องการส่งทีละ field:
| Key | Fieldname |
|---|---|
invoice_name | name |
customer | customer |
total | grand_total |
currency | currency |
ใช้ Jinja template กำหนด JSON structure:
เพิ่ม custom headers สำหรับ authentication:
| Key | Value |
|---|---|
Content-Type | application/json |
Authorization | Bearer your-api-token |
X-Source | erpnext |
เปิดใช้ Webhook Secret เพื่อให้ฝั่งรับสามารถ verify ว่า request มาจาก Frappe จริง:
X-Frappe-Webhook-Signature ที่มี HMAC-SHA256 hash ของ payload# ฝั่งรับ — verify webhook signature (Python example)
import hmac
import hashlib
import base64
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = base64.b64encode(
hmac.new(
secret.encode(),
payload,
hashlib.sha256
).digest()
).decode()
return hmac.compare_digest(expected, signature)เมื่อสร้าง Sales Invoice ใน ERPNext → ส่งข้อความแจ้งเตือนไปยัง Line Group ผ่าน Line Notify
| Field | Value |
|---|---|
| DocType | Sales Invoice |
| Document Event | on_submit |
| Request URL | https://notify-api.line.me/api/notify |
| Request Method | POST |
| Key | Value |
|---|---|
Authorization | Bearer YOUR_LINE_NOTIFY_TOKEN |
Content-Type | application/x-www-form-urlencoded |
| Key | Value |
|---|---|
message | \nใบแจ้งหนี้ใหม่: {{ doc.name }}\nลูกค้า: {{ doc.customer }}\nยอดรวม: ฿{{ doc.grand_total }} |
เมื่ออัพเดท Stock Entry → ส่งข้อมูลสต็อกไปยัง warehouse management system
ดู log ของ webhook request ที่ส่งไปแล้วได้ที่ /app/webhook-request-log:
ใช้สำหรับ debug เมื่อ webhook ไม่ทำงานตามที่คาด
Checklist:
bench --site {site} enable-scheduler| Status | สาเหตุ | วิธีแก้ |
|---|---|---|
| 400 | JSON format ผิด | ตรวจ Jinja template, ใช้ {{ doc.field | tojson }} สำหรับ string ที่มี special characters |
| 401 | Auth ไม่ถูกต้อง | ตรวจ Authorization header |
| 403 | IP ถูก block | เพิ่ม server IP ใน allowlist ฝั่งรับ |
| 500 | Server error ฝั่งรับ | ตรวจ log ฝั่งรับ |
| 504 | Timeout | ฝั่งรับ process นานเกินไป, ใช้ queue |
# ❌ ผิด — string ที่มี quote จะ break JSON
{"customer": "{{ doc.customer }}"}
# ✅ ถูก — ใช้ tojson filter
{"customer": {{ doc.customer | tojson }}}Frappe ส่ง webhook ครั้งเดียวต่อ event ไม่มี automatic retry ถ้าต้องการ retry ต้อง implement ฝั่งรับเอง หรือใช้ Custom Script trigger manual
ได้ Webhook รองรับทุก DocType ทั้ง standard และ custom
ไม่มี built-in rate limit แต่ถ้ามี event จำนวนมาก (เช่น bulk import) webhook จะ trigger ทุก document ควรใส่ condition เพื่อ filter
Webhook trigger ผ่าน Frappe's event hooks ซึ่งทำงานใน request context ถ้า document ถูกสร้างผ่าน background job webhook จะยัง trigger ปกติ
ถ้าต้องการความช่วยเหลือในการ setup Webhook, API Integration หรือ Custom Development สำหรับ Frappe/ERPNext
รับคำปรึกษาฟรี หรือ ดูบริการ ERPNext Customization ของเรา
{
"event": "invoice_submitted",
"invoice": "{{ doc.name }}",
"customer": "{{ doc.customer }}",
"grand_total": {{ doc.grand_total }},
"currency": "{{ doc.currency }}",
"items": [
{% for item in doc.items %}
{
"item_code": "{{ item.item_code }}",
"qty": {{ item.qty }},
"rate": {{ item.rate }}
}{% if not loop.last %},{% endif %}
{% endfor %}
],
"submitted_at": "{{ frappe.utils.now() }}"
}// ฝั่งรับ — verify webhook signature (Node.js/TypeScript)
import crypto from "node:crypto";
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}{
"action": "stock_update",
"entry": "{{ doc.name }}",
"stock_entry_type": "{{ doc.stock_entry_type }}",
"posting_date": "{{ doc.posting_date }}",
"items": [
{% for item in doc.items %}
{
"item_code": "{{ item.item_code }}",
"qty": {{ item.qty }},
"warehouse": "{{ item.t_warehouse or item.s_warehouse }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}