English
English
Appearance
English
English
Appearance
Each store can register multiple endpoints (multiple subscriptions) to receive webhooks. Each subscription includes:
| field | note |
|---|---|
| url | The URL of your endpoint that will receive webhooks (POST is sent here) |
| secret | The secret key used to sign (HMAC) the request, shown only once at creation time — you must save it to verify the signature |
| event_types | The list of events you want to receive. Leave empty = receive all event types |
| is_active | Whether the subscription is currently active |
Method: POST to the registered
url
Header
| key | value |
|---|---|
| Content-Type | application/json |
| X-NextFF-Event | The internal event type. Currently available: order.status_changed (order status changed), webhook.ping (sent when you click the Test button on the dashboard) |
| X-NextFF-Delivery | An identifier for this delivery (used to prevent duplicate processing) |
| X-NextFF-Timestamp | Unix timestamp (in seconds) at the time the request is sent, used to verify the signature |
| X-NextFF-Signature | The HMAC-SHA256 signature of the request, in the form sha256=<hex> |
Webhook Event (body)
| key | value |
|---|---|
| type | event name (order_update) |
| reference_id | the order_number of the order |
| order_id | the internal ID (uuid) of the order |
Example (Request)
{
"type": "order_update",
"reference_id": "ORDER_NUMBER",
"order_id": "d33e0745-***-9fb1-01ac3e5135c3"
}The order_update event is sent whenever an order's status changes in the system (not only when the order update API above is called, but any time the order is updated on the Pawdo side).
Event
webhook.ping
Sent when you click the Test button on a subscription in the Pawdo dashboard — used to check whether your endpoint can receive and verify the signature; it is not tied to any order. The payload is different from order_update (no reference_id/order_id):
{
"type": "webhook.ping",
"message": "This is a test webhook delivery"
}Note: the
webhook.pingevent is sent regardless of whether your subscription declares a specificevent_typesfilter (Test always bypasses this filter) — if you only subscribe toorder.status_changedbut still seewebhook.pingarrive, that is because of clicking Test, not a filter bug.
Because the system may add new event types in the future, when implementing your receiver you should handle them in a "safe ignore" manner (ignore + return
200) for anytypeyou do not recognize, instead of treating it as an error.
To verify that a webhook request truly comes from Pawdo (and has not been forged), recompute the signature yourself and compare it with the X-NextFF-Signature header:
signature = hex( HMAC_SHA256( secret, "{timestamp}." + raw_request_body ) )Where:
secret: the secret key provided when registering the subscriptiontimestamp: taken from the X-NextFF-Timestamp headerraw_request_body: the entire raw (unparsed) body of the requestCompare the computed hex string with the part after sha256= in the X-NextFF-Signature header. If it does not match, reject the request.
Example: verify signature in Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
// webhookSecret is the secret received when creating the webhook subscription (shown only once at creation)
const webhookSecret = "your_webhook_secret"
// verifySignature recomputes the signature using the exact formula Pawdo uses to sign:
// hex(HMAC_SHA256(secret, "{timestamp}." + raw_body))
// then compares it with the X-NextFF-Signature header using hmac.Equal (constant-time, protects against timing attacks).
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...) and handle your business logic
w.WriteHeader(http.StatusOK)
}Note: always use
hmac.Equal(or an equivalent constant-time compare) to compare the signature, do not use a regular==/strings.Compare— this avoids leaking information through processing time (timing attack). You must also read therawBodybefore parsing the JSON, because the signature is signed over the entire original body, not over the re-parsed object.
If your endpoint returns a status code outside the 2xx range, or the request times out / has a connection error, the system will automatically retry (with backoff) according to the internal queue configuration.
If you need further assistance regarding webhook information or usage, please contact the Pawdo support team via Facebook for assistance.