Order Completion Webhooks for Printie Partners
Implement Printie's order.completed webhook safely with the current payload, durable intake, idempotency, testing, and failure monitoring.
By Tyler Reece · Published November 20, 2025 · Updated July 22, 2026 · 6 min read
Printie's order-completion webhook sends a JSON POST after a fulfillment record is written. It gives a partner system the completed order identity, shipment and tracking data, totals, SKU lines, assigned design files, and included-item details without requiring polling.
The important operating detail is delivery behavior: Printie waits up to 10 seconds, does not automatically retry, and never rolls back physical fulfillment because a partner endpoint failed. Your receiver should therefore validate quickly, store the event durably, return a successful HTTP response, and process downstream work asynchronously.
This feature appears in the Advanced section of Account Settings only for accounts with partner API access. It is separate from the Shippo or ShipStation connection used for storefront order intake.
Configure and test the endpoint
In Account Settings → Advanced → Order completion webhook, save an HTTP or HTTPS URL. Production receivers should use HTTPS. A blank value disables delivery. URLs are limited to 2,000 characters, and Printie rejects malformed URLs, unsupported protocols, literal private-network targets, loopback hosts, and internal hostnames.
Use Send test before the first live fulfillment. The test invokes the same outbound delivery path with representative order, tracking, totals, selected-filament, and included-item data. A failed destination response is returned as a structured result such as a timeout or HTTP status instead of turning the test request into an unrelated server failure.
Testing should prove more than “the route returned 200.” Confirm that:
- the receiver stores the raw body and arrival time;
- the event can be identified more than once without duplicating side effects;
- unknown optional fields do not break parsing;
- sensitive customer fields do not leak into unrestricted logs;
- downstream jobs can be replayed from the stored event;
- an intentional receiver failure appears in your alerting.
Current payload contract
The event name is order.completed and the current payload version is 2026-07-06. Treat the version as a schema identifier, not as the date an individual order was completed.
Here is a shortened but structurally accurate example:
{
"event": "order.completed",
"version": "2026-07-06",
"fulfillmentId": "account-123_order-456",
"completedAt": "2026-07-22T18:45:00.000Z",
"order": {
"orderId": "order-456",
"orderNumber": "A-456",
"status": "Completed",
"customer": {
"name": "Example Customer",
"email": "customer@example.com"
},
"shippingAddress": {
"name": "Example Customer",
"addressLine1": "123 Main St",
"addressLine2": null,
"city": "Boston",
"state": "MA",
"postalCode": "02118",
"country": "United States"
},
"shipping": {
"carrier": "ups",
"trackingNumber": "1Z999",
"trackingUrl": "https://example.com/tracking/1Z999",
"labelUrl": "https://example.com/labels/label.pdf",
"cost": 10
},
"totals": {
"totalQuantity": 1,
"totalCost": 25.5,
"productionCost": 15,
"shippingCost": 10,
"includedItemsPickFee": 0.5
},
"items": [
{
"sku": "STORE-SKU-1",
"title": "Desk Stand",
"quantity": 1,
"unitPrice": 0,
"includedItems": [
{
"itemId": "thank-you-card",
"name": "Thank You Card",
"quantityPerUnit": 1,
"usage": "include",
"instructions": "Place on top of the print"
}
],
"designFiles": [
{
"designId": "design-123",
"designName": "Desk Stand v2",
"quantity": 1,
"filamentType": "PLA",
"selectedFilaments": [],
"notes": null,
"printProfile": null,
"costData": {
"totalCost": 15
}
}
]
}
]
},
"metadata": {
"partner": null
}
}Authorized Printie partners can open the machine-readable contract in the gated /api-docs reference after an API key is enabled. Contact Printie if your account needs partner API access. Generate types or fixtures from that contract when possible, but keep your parser tolerant of additional fields. A new optional property should not make a previously valid completion event unreadable.
Design the receiver as an inbox
The safest receiver is a small inbox rather than a large workflow:
- Accept the POST.
- Enforce a reasonable body-size limit.
- Parse JSON and require the expected event name.
- Derive an idempotency key.
- Insert the event and key into durable storage in one transaction.
- Return a 2xx response.
- Let a background worker update the CRM, ERP, analytics, or customer system.
Use fulfillmentId + event + version as a practical event key. If your business permits multiple completion records for the same source order, do not deduplicate on orderNumber alone. Order numbers can collide across stores, and a human-readable number is not necessarily the stable record identity.
An idempotent consumer is still necessary even though Printie does not currently retry automatically. Operators can send test events repeatedly, reverse proxies can retry requests, and future delivery behavior can evolve. Shippo's own tracking documentation explicitly notes that its tracking webhooks are not idempotent, which illustrates the broader rule: webhook consumers should assume duplicate delivery is possible.
Return success only after durable acceptance
Do not return 200 before the event is stored. If the process exits between the response and the database write, there is no automatic Printie retry to recover that completion.
Do not perform a slow sequence of external API calls before responding either. Printie's request timeout is 10 seconds. A CRM outage or spreadsheet API delay should not turn ingestion into a timeout. Store first, acknowledge, then retry your own downstream jobs under your own policy.
The HTTP specification defines successful responses as the 2xx class; see RFC 9110, section 15.3. Use a clear 2xx response once the inbox owns the event. Reserve 4xx for an event you intentionally reject and 5xx for a receiver failure, knowing that the current sender will report the failure but will not schedule another attempt.
Handle totals deliberately
Do not assume every cost field means storefront revenue.
- totalCost represents fulfillment cost in the completion record.
- productionCost represents charged production amounts when available.
- shippingCost can be null on legacy records.
- includedItemsPickFee covers applicable picked customer-supplied items.
- unitPrice is not a promise of the buyer's retail price.
If the webhook feeds accounting, preserve the original payload and map each field explicitly into your ledger. Do not silently substitute zero for null: “not available” and “no cost” are different facts.
The items array carries the SKU-level quantity and assigned production details. A designFiles quantity is the quantity of that design per sold line configuration; the line's own quantity still matters. Test a multi-quantity order before using these fields for inventory or unit economics.
Protect customer and fulfillment data
The payload can contain a name, email, full shipping address, label URL, tracking number, and internal cost information. Apply the same access controls and retention policy you use for orders.
- Keep raw bodies out of public error trackers.
- Redact addresses and label URLs from routine logs.
- Use a dedicated secret endpoint path or gateway controls.
- Restrict database access by service role.
- Rotate any URL token that is exposed.
- Validate outbound actions; never treat notes or titles as executable instructions.
If your gateway supports request signing of your own, that does not prove Printie generated the body unless both systems share a verification contract. Do not invent a signature header that the current webhook does not send. Use network controls and a secret endpoint as compensating controls, then contact Printie if your integration requires a stronger authentication arrangement.
Monitor the two halves separately
Create one dashboard for intake and another for downstream processing:
Signal | Question it answers |
|---|---|
| Received events | Did the endpoint accept completions? |
| Duplicate-key count | Are events being delivered or replayed more than once? |
| Receiver response time | Is ingestion safely below the timeout? |
| Queue age | Are accepted events waiting too long? |
| Permanent processing failures | Which system or mapping needs intervention? |
| Order reconciliation gap | Which Printie completions are absent downstream? |
Because delivery is best-effort, schedule a periodic reconciliation against your authoritative Printie fulfillment data when the integration is business-critical. The webhook reduces latency; reconciliation closes gaps.
Production-readiness checklist
- Partner API access exposes the Advanced settings.
- The endpoint is HTTPS and publicly reachable.
- The Send test payload is stored and processed.
- Parsing accepts version 2026-07-06 and ignores unknown optional fields.
- fulfillmentId-based idempotency is enforced in durable storage.
- A 2xx response happens only after the event is committed.
- Background processing has retries and a dead-letter path.
- Logs redact customer, address, label, and cost data.
- Multi-SKU and multi-quantity totals have been tested.
- Monitoring catches both delivery failures and stuck downstream jobs.
- Reconciliation can identify a completion missing from the partner system.
For the upstream order path, read the automated fulfillment blueprint. To understand the production and shipping stages that precede this event, see How Printie works and review current pricing.