Appearance
Webhook events
Outbound webhooks POST to your URL when something happens in your shop. Setup is in webhook subscribers. This page is the payload reference.
Available on Premium and above.
The envelope
Every delivery has the same shape:
json
{
"event": "order.stage_changed",
"data": { },
"sent_at": "2026-08-25T09:14:22+00:00"
}With these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-PF-Event | The event name |
X-PF-Signature | sha256= plus the HMAC |
User-Agent | PrintersFriend-Webhooks/1.0 |
Events
| Event | Fires when |
|---|---|
order.stage_changed | An order moves stage, including a cancellation |
order.dispatched | An order reaches Dispatched |
invoice.issued | An invoice is issued |
invoice.paid | An invoice is paid |
artwork.approved | A customer approves artwork |
* | Subscribe to all of the above |
order.stage_changed and order.dispatched
json
{
"event": "order.stage_changed",
"data": {
"order_number": "ORD-2026-0001",
"from_stage": "print_queue",
"to_stage": "in_production",
"customer": "Northside Football Club",
"due_by": "2026-08-28T00:00:00+00:00"
},
"sent_at": "2026-08-25T09:14:22+00:00"
}A cancellation arrives as order.stage_changed with to_stage of cancelled.
Reaching Dispatched sends order.dispatched rather than order.stage_changed, so subscribe to both if you want every move including the last one.
Verifying a delivery
The signature is an HMAC-SHA256 of the raw request body, keyed with your subscriber's secret, hex encoded, prefixed with sha256=.
php
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (! hash_equals($expected, $request->header('X-PF-Signature'))) {
abort(401);
}javascript
const crypto = require('crypto')
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))Three rules:
- Hash the raw body, not a re-serialised object. Re-encoding JSON changes the bytes and the signature will never match.
- Compare in constant time (
hash_equals,timingSafeEqual). - Reject on mismatch. An endpoint that accepts unsigned payloads accepts anyone's payloads.
Retries and failure handling
| Behaviour | Detail |
|---|---|
| Timeout | 10 seconds |
| Retries | 5 attempts |
| Backoff | 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours |
| Success | Any 2xx |
| Repeated failure | After 20 consecutive failures the subscriber is deactivated |
Every attempt is recorded as a delivery with the status code and the first 1,000 characters of your response, which is what to look at when debugging.
Writing a good endpoint
- Be idempotent. The same event can arrive more than once. Key on the event plus the order number and ignore duplicates.
- Return 2xx fast. Acknowledge, then do the work asynchronously. Long processing inside the request will hit the 10 second timeout and be retried.
- Return a useful body on error. It is stored against the delivery and it is what tells you what went wrong.
- Do not trust the payload without verifying the signature.
Plan behaviour
With webhooks not enabled on your plan, subscribers stay saved and deliveries stop, with the reason logged rather than silently dropped. Moving back to Premium or above resumes them. Nothing is deleted.