Guide to Printify Webhooks: Features and Best Practices
Printify webhooks tell your app what is happening in a merchant's shop: an order is created, sent to production, shipped or delivered, a line item is cancelled, a product is published or deleted, or the shop disconnects. If you run a custom storefront or an app connected to Printify, webhooks are how you keep orders, tracking and listings in sync without polling the API.
This guide covers how Printify webhooks are created and signed, the events and their payloads, Printify's retry and blocking rule, and the practices that keep a Printify receiver safe and reliable.
What are Printify webhooks?
A Printify webhook is a subscription to one event type, called a topic, in one shop. There is no dashboard for them: you create, list, change and delete webhooks through the Public API, under /v1/shops/{shop_, with a token that has the webhooks.read and webhooks.write scopes.
When the event happens, Printify sends a POST with a JSON body to the webhook's URL. If you gave the webhook a secret, the request also carries an X-Pfy-Signature header: an HMAC-SHA256 of the raw body, keyed with your secret. The secret is optional, and a webhook created without one sends no signature.
Printify webhook features
| Feature | Details |
|---|---|
| Configuration | Public API only, one webhook per shop and topic |
| Method | POST with a JSON body |
| Signature header | X-Pfy-Signature, formatted sha256=<digest> |
| Algorithm | HMAC-SHA256 of the raw request body, hex-encoded |
| Secret | Optional, chosen by you when you create or modify the webhook |
| Replay window | None. No timestamp is signed |
| Expected response | 200 OK |
| Retries | Up to 3 on a 4xx or 5xx response |
| Blocking | After the retries, the webhook is blocked for 1 hour and nothing is sent to its URL |
| Testing | A simulate endpoint sends a test event to a webhook |
Printify documents no response timeout, delivery ID header or source IP addresses. Its docs mention allowlisting Printify's IP address as an option, but don't publish one.
Common events
Printify sends 12 events, named in the body's type field. No header carries the event type, but because each webhook has a single topic, the webhook's URL can identify it too.
| Resource | Event | Fires when |
|---|---|---|
| Order | order:created | An order is created |
| Order | order:updated | An order's status changes |
| Order | order:line-item:canceled | A single line item is cancelled, by the merchant, support or the system |
| Order | order:sent-to-production | An order is sent to production |
| Order | order:shipment:created | Some or all items have been fulfilled |
| Order | order:shipment:delivered | Some or all items have been delivered |
| Product | product:created | A product is created |
| Product | product:updated | A product is updated |
| Product | product:deleted | A product is deleted |
| Product | product:publish:started | Publishing a product to the storefront has started |
| Shop | shop:disconnected | The shop is disconnected |
| Personalisation | personalization-preview-task:processed | A personalisation preview task completes, is corrupt or fails |
Every payload has four top-level keys: id, type, created_at and resource. Here is Printify's order:shipment:created example:
{
"id": "653b6be8-2ff7-4ab5-a7a6-6889a8b3bbf5",
"type": "order:shipment:created",
"created_at": "2022-05-17 15:00:00+00:00",
"resource": {
"id": "5a96f649b2439217d070f507",
"type": "order",
"data": {
"shop_id": 815256,
"shipped_at": "2022-05-17 15:00:00+00:00",
"carrier": {
"code": "USPS",
"tracking_number": "9400110200828911663274",
"tracking_url": "https://example.com/track/9400110200828911663274"
},
"skus": [
"6202"
]
}
}
}
id identifies the event, and resource.id the order, product, shop or preview task it concerns. resource.data changes with the event: order:updated carries the new status, order:line-item:canceled carries line_item_id and a status_details reason such as out_of_stock, and shop:disconnected sends no data. If the cancelled line item was the order's last, Printify also sends order:updated for the order.
product:publish:started carries an action of create, update or delete. For create and update, publish_details says which parts of the product to publish:
{
"id": "653b6be8-2ff7-4ab5-a7a6-6889a8b3bbf5",
"type": "product:publish:started",
"created_at": "2022-05-17 15:00:00+00:00",
"resource": {
"id": "5cb87a8cd490a2ccb256cec4",
"type": "product",
"data": {
"shop_id": 815256,
"publish_details": {
"title": true,
"variants": false,
"description": true,
"tags": true,
"images": false,
"key_features": false,
"shipping_template": true
},
"action": "create",
"out_of_stock_publishing": 0
}
}
}
A storefront integration reports the outcome back through the API's publishing_ or publishing_failed.json endpoint for the product.
Note that created_at and the other timestamps use a space between date and time (2022-05-17 15:00:00+00:00) rather than ISO 8601's T. Some strict date parsers reject them.
Setting up Printify webhooks
- Find your shop's ID with
GET /v1/shops.json. - Generate a secret, for example with
openssl rand -hex 20as Printify suggests, and store it server-side asPRINTIFY_WEBHOOK_SECRET. - Create one webhook per topic you need, with the request below.
- Test it with
POST /v1/shops/{shop_. Whatever you send in the request body comes back in the event'sid}/webhooks/{webhook_ id}/simulate resourceproperty.
curl -X POST "https://api.printify.com/v1/shops/$SHOP_ID/webhooks.json" \
-H "Authorization: Bearer $PRINTIFY_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"topic\":\"order:shipment:created\",\"url\":\"https://example.com/webhooks/printify\",\"secret\":\"$PRINTIFY_WEBHOOK_SECRET\"}"
A webhook's topic can't be changed, but you can change its URL or secret with PUT /v1/shops/{shop_. Deleting one requires a host query parameter that matches the webhook URL's host, which stops you from deleting another app's webhooks by mistake.
If you receive through Hookdeck, create a Printify source, use its URL as each webhook's url, and paste the secret into the source's Webhook Signing Secret field. Use the same secret for every topic that points at the source. Verification in Hookdeck is optional: leave it off if you created your webhooks without a secret.
Securing Printify webhooks
Compute an HMAC-SHA256 of the raw request body with your secret, encode it as hex, and compare it in constant time with the part of X-Pfy-Signature after sha256=. Use the secret as a plain UTF-8 string, without decoding it, and verify against the original bytes, not re-serialised JSON.
const crypto = require('crypto');
const express = require('express');
function verifyPrintifySignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const value = signatureHeader.trim();
if (!value.startsWith('sha256=')) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(value.slice('sha256='.length).toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/printify', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.PRINTIFY_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyPrintifySignature(req.body, req.get('x-pfy-signature'), secret)) {
return res.sendStatus(401);
}
let body;
try {
body = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Route on body.type. Deduplicate on body.id.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_printify_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
value = signature_header.strip()
if not value.startswith("sha256="):
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
received = value[len("sha256="):].lower().encode("utf-8")
return hmac.compare_digest(received, expected.encode("utf-8"))
# In FastAPI: verify_printify_signature(await request.body(),
# request.headers.get("x-pfy-signature"), os.environ["PRINTIFY_WEBHOOK_SECRET"])
Only the body is signed, so a captured request can be replayed and still verify. Deduplicate on the event id and confirm important state, such as an order's status, with the API before acting on it.
Be careful with error codes while you set this up. A 401 from a misconfigured secret counts as a failure like any other 4xx, so after three retries Printify blocks the webhook for an hour. Use the simulate endpoint to confirm verification passes before you point real traffic at the endpoint.
Make Printify webhooks production-ready. Hookdeck Event Gateway verifies the
X-Pfy-Signatureheader, answers Printify with a200straight away, and durably queues every order, shipment and product event before your service sees it.
Printify webhook limitations and pain points
An hour of silence after three failures
The Problem: A short outage or a bad deploy stops all deliveries to a webhook for an hour, and events that happen during that hour never reach you.
Why It Happens: On a 4xx or 5xx response Printify retries only three times, then blocks the webhook for an hour and sends nothing to its URL until the block lifts.
Workarounds:
- Return
200as soon as the request is verified and stored, and do the work afterwards. - Never return an error for an event type you don't handle. Acknowledge it and ignore it.
- After an outage, reconcile with the API: list recent orders and products and compare them with what you've stored.
How Hookdeck Can Help: Hookdeck answers Printify as soon as each event arrives, so your service's downtime doesn't count as a failed delivery and can't get the webhook blocked. Hookdeck then retries delivery to your service on a schedule you set and lets you replay anything that failed.
One webhook per topic
The Problem: Covering every event means creating and maintaining a dozen webhooks per shop, and an app connected to many shops multiplies that.
Why It Happens: Each webhook subscribes to exactly one topic, in one shop, and its topic can't be changed. There is no dashboard, so everything goes through the API.
Workarounds:
- Script webhook creation, and point every topic at the same URL, routing on
type. - Use the list endpoint to check each shop's webhooks match what you expect, and recreate any that are missing.
How Hookdeck Can Help: Point every webhook at one Hookdeck source, then use filters on type to route each event to the right destination, without changing the webhooks in Printify.
Signing is optional and has no replay protection
The Problem: A webhook created without a secret can't be authenticated, and a signed request can be replayed.
Why It Happens: The secret property is optional, and the signature covers only the body, with no timestamp.
Workarounds:
- Always create webhooks with a secret. To add one to an existing webhook, update it with
PUT. - Deduplicate on the event
id.
How Hookdeck Can Help: Hookdeck checks X-Pfy-Signature on every request, and its deduplication can drop repeats of the same id before they reach your service.
Thin payloads
The Problem: Your handler knows an order changed, but not what the order now contains.
Why It Happens: Most events carry only a few fields. order:created and order:sent-to-production are documented as sending no resource data, and product events carry only shop_id.
Workarounds:
- Treat events as notifications: fetch the order or product from the API by
resource.id. - Read
resource.datadefensively. Printify's own examples fororder:createdandorder:sent-to-productioninclude ashop_idindataeven though the event tables say no data is sent.
How Hookdeck Can Help: Hookdeck's queue absorbs bursts, such as a batch of orders going to production, and its rate limiting lets you pace delivery so your follow-up API calls stay within Printify's limits.
Best practices
Verify against the raw body
Capture the body as bytes, verify X-Pfy-Signature, and only then parse it.
Acknowledge fast and process asynchronously
Store the event and return 200 before calling the Printify API or updating your storefront, so a slow dependency never turns into a blocked webhook. See why to process webhooks asynchronously.
Make handlers idempotent
Use the event id as the key, and make updates safe to apply twice, for example by setting an order's status rather than appending to a history. See our guide to webhook idempotency.
Fetch the full resource
Use resource.id to retrieve the current order or product from the API rather than relying on the event's few fields.
Test with the simulate endpoint
Send a simulated event after creating or changing a webhook, and after rotating a secret, to confirm your endpoint verifies and answers 200.
Conclusion
Printify webhooks are JSON POSTs, one topic per webhook, created through the API and signed with a hex HMAC-SHA256 of the body in X-Pfy-Signature when you set a secret. Always set one, verify against the raw body, deduplicate on the event id, fetch full resources from the API, and acknowledge quickly, because three failed retries block the webhook for an hour.
Hookdeck Event Gateway sits in front of your receiver, verifies Printify's signature, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Printify webhooks reliably in minutes.