Skip to content

Webhook

Registering to receive webhooks

Each store can register multiple endpoints (multiple subscriptions) to receive webhooks. Each subscription includes:

fieldnote
urlThe URL of your endpoint that will receive webhooks (POST is sent here)
secretThe secret key used to sign (HMAC) the request, shown only once at creation time — you must save it to verify the signature
event_typesThe list of events you want to receive. Leave empty = receive all event types
is_activeWhether the subscription is currently active

Sending webhooks

Method: POST to the registered url

Header

keyvalue
Content-Typeapplication/json
X-NextFF-EventThe 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-DeliveryAn identifier for this delivery (used to prevent duplicate processing)
X-NextFF-TimestampUnix timestamp (in seconds) at the time the request is sent, used to verify the signature
X-NextFF-SignatureThe HMAC-SHA256 signature of the request, in the form sha256=<hex>

Webhook Event (body)

keyvalue
typeevent name (order_update)
reference_idthe order_number of the order
order_idthe internal ID (uuid) of the order

Example (Request)

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

json
{
  "type": "webhook.ping",
  "message": "This is a test webhook delivery"
}

Note: the webhook.ping event is sent regardless of whether your subscription declares a specific event_types filter (Test always bypasses this filter) — if you only subscribe to order.status_changed but still see webhook.ping arrive, 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 any type you do not recognize, instead of treating it as an error.

Verify signature

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 subscription
  • timestamp: taken from the X-NextFF-Timestamp header
  • raw_request_body: the entire raw (unparsed) body of the request

Compare 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

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 the rawBody before parsing the JSON, because the signature is signed over the entire original body, not over the re-parsed object.

Retry

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.