Skip to content

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:

fieldnote
urlURL endpoint của bạn sẽ nhận webhook (POST tới đây)
secretKhoá 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_typesDanh sách event muốn nhận. Để trống = nhận tất cả các loại event
is_activeTrạng thái subscription có đang hoạt động không

Gửi webhook

Method: POST tới url đã đăng ký

Header

keyvalue
Content-Typeapplication/json
X-NextFF-EventLoạ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-DeliveryID định danh cho lượt gửi này (dùng để chống xử lý trùng lặp)
X-NextFF-TimestampUnix timestamp (giây) lúc gửi request, dùng để verify signature
X-NextFF-SignatureChữ ký HMAC-SHA256 của request, dạng sha256=<hex>

Webhook Event (body)

keyvalue
typetên sự kiện (order_update)
reference_idorder_number của đơn hàng
order_idID nội bộ (uuid) của đơn hàng

Example (Request)

json
{
  "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):

json
{
  "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áo event_types giới hạn cụ thể hay không (test luôn bỏ qua filter này) — nếu bạn chỉ subscribe order.status_changed mà vẫn thấy webhook.ping tớ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ới type khô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ý subscription
  • timestamp: lấy từ header X-NextFF-Timestamp
  • raw_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

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.Compare thông thường — tránh lộ thông tin qua thời gian xử lý (timing attack). Đồng thời phải đọc rawBody trướ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ợ.