Skip to content

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:

HeaderValue
Content-Typeapplication/json
X-PF-EventThe event name
X-PF-Signaturesha256= plus the HMAC
User-AgentPrintersFriend-Webhooks/1.0

Events

EventFires when
order.stage_changedAn order moves stage, including a cancellation
order.dispatchedAn order reaches Dispatched
invoice.issuedAn invoice is issued
invoice.paidAn invoice is paid
artwork.approvedA 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:

  1. Hash the raw body, not a re-serialised object. Re-encoding JSON changes the bytes and the signature will never match.
  2. Compare in constant time (hash_equals, timingSafeEqual).
  3. Reject on mismatch. An endpoint that accepts unsigned payloads accepts anyone's payloads.

Retries and failure handling

BehaviourDetail
Timeout10 seconds
Retries5 attempts
Backoff1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours
SuccessAny 2xx
Repeated failureAfter 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.

Printer's Friend - software for apparel print shops