Webhook
Đăng ký nhận webhook
Mỗi store có thể đăng ký nhiều endpoint (nhiều subscription) để nhận webhook, mỗi subscription gồm:
| field | note |
|---|---|
| url | URL endpoint của bạn sẽ nhận webhook (POST tới đây) |
| secret | Khoá bí mật dùng để ký (HMAC) request, chỉ hiển thị 1 lần duy nhất lúc tạo — cần lưu lại để verify signature |
| event_types | Danh sách event muốn nhận. Để trống = nhận tất cả các loại event |
| is_active | Trạng thái subscription có đang hoạt động không |
Gửi webhook
Method: POST tới
urlđã đăng ký
Header
| key | value |
|---|---|
| Content-Type | application/json |
| X-NextFF-Event | Loại event nội bộ. Hiện có: order.status_changed (đơn hàng thay đổi trạng thái), webhook.ping (gửi khi bạn bấm nút Test trên dashboard) |
| X-NextFF-Delivery | ID định danh cho lượt gửi này (dùng để chống xử lý trùng lặp) |
| X-NextFF-Timestamp | Unix timestamp (giây) lúc gửi request, dùng để verify signature |
| X-NextFF-Signature | Chữ ký HMAC-SHA256 của request, dạng sha256=<hex> |
Webhook Event (body)
| key | value |
|---|---|
| type | tên sự kiện (order_update) |
| reference_id | order_number của đơn hàng |
| order_id | ID nội bộ (uuid) của đơn hàng |
Example (Request)
{
"type": "order_update",
"reference_id": "ORDER_NUMBER",
"order_id": "d33e0745-***-9fb1-01ac3e5135c3"
}Sự kiện order_update được gửi mỗi khi trạng thái đơn hàng thay đổi trong hệ thống (không chỉ riêng khi gọi API cập nhật đơn hàng ở trên, mà bất kỳ khi nào order được cập nhật ở phía Pawdo).
Event
webhook.ping
Được gửi khi bạn bấm nút Test trên subscription ở dashboard Pawdo — dùng để kiểm tra endpoint của bạn có nhận và verify được signature hay không, không gắn với đơn hàng nào. Payload khác với order_update (không có reference_id/order_id):
{
"type": "webhook.ping",
"message": "This is a test webhook delivery"
}Lưu ý: event
webhook.pingđược gửi bất kể subscription của bạn có khai báoevent_typesgiới hạn cụ thể hay không (test luôn bỏ qua filter này) — nếu bạn chỉ subscribeorder.status_changedmà vẫn thấywebhook.pingtới, đó là do bấm Test, không phải lỗi filter.
Vì hệ thống có thể bổ sung event type mới trong tương lai, khi implement receiver nên xử lý theo kiểu "bỏ qua an toàn" (ignore + trả
200) vớitypekhông nhận diện được, thay vì coi là lỗi.
Verify signature
Để xác thực request webhook thực sự đến từ Pawdo (và không bị giả mạo), hãy tự tính lại signature và so sánh với header X-NextFF-Signature:
signature = hex( HMAC_SHA256( secret, "{timestamp}." + raw_request_body ) )Trong đó:
secret: khoá bí mật được cấp khi đăng ký subscriptiontimestamp: lấy từ headerX-NextFF-Timestampraw_request_body: toàn bộ body raw (chưa parse) của request
So sánh chuỗi hex tính được với phần sau sha256= trong header X-NextFF-Signature. Nếu không khớp, hãy từ chối (reject) request.
Ví dụ verify signature bằng Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
// webhookSecret là secret nhận được lúc tạo webhook subscription (chỉ hiển thị 1 lần lúc tạo)
const webhookSecret = "your_webhook_secret"
// verifySignature tính lại signature theo đúng công thức Pawdo dùng để ký:
// hex(HMAC_SHA256(secret, "{timestamp}." + raw_body))
// rồi so sánh với header X-NextFF-Signature bằng hmac.Equal (constant-time, chống timing attack).
func verifySignature(secret, timestamp string, rawBody []byte, signatureHeader string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
got := strings.TrimPrefix(signatureHeader, "sha256=")
return hmac.Equal([]byte(expected), []byte(got))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
timestamp := r.Header.Get("X-NextFF-Timestamp")
signature := r.Header.Get("X-NextFF-Signature")
if !verifySignature(webhookSecret, timestamp, rawBody, signature) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// TODO: parse rawBody (type, reference_id, order_id...) và xử lý nghiệp vụ
w.WriteHeader(http.StatusOK)
}Lưu ý: luôn dùng
hmac.Equal(hoặc tương đương constant-time compare) để so sánh signature, không dùng==/strings.Comparethông thường — tránh lộ thông tin qua thời gian xử lý (timing attack). Đồng thời phải đọcrawBodytrước khi parse JSON, vì signature được ký trên toàn bộ body gốc, không phải trên object đã parse lại.
Retry
Nếu endpoint của bạn trả về status code ngoài khoảng 2xx, hoặc request bị timeout/lỗi kết nối, hệ thống sẽ tự động retry (có backoff) theo cấu hình hàng đợi nội bộ.
Nếu cần hỗ trợ thêm về thông tin hoặc cách sử dụng webhook, vui lòng liên hệ đội ngũ hỗ trợ Pawdo qua Facebook để được hỗ trợ.