Store & Fulfillment
Run the boilerplate as a single-owner store and auto-fulfil print-on-demand orders through Printful — API token, product sync, the paid-order → Printful flow, and shipment tracking.
Why this matters
Flip the boilerplate into the store format and it stops being a catalogue and becomes a shop: items are products with a price and stock, and the CTA is "buy" via native Stripe Checkout. With print-on-demand (POD) you never touch inventory — the buyer pays, the order is recorded, and it's submitted to Printful, which prints and ships it to your customer. Tracking flows back automatically.
This page wires that fulfilment end-to-end. It assumes the store format is already on — if it isn't, start with Marketplace Config.
The Printful order is submitted inside the Stripe webhook handler, right after payment succeeds. If your Stripe webhook isn't configured and verified, no order is recorded and nothing reaches Printful. Set that up first — see Payments → Step 4.
Before you start
Three prerequisites, all required:
- Store format on —
vertical: 'store'+store: true. See Marketplace Config. - Stripe webhook working —
STRIPE_WEBHOOK_SECRETset and the endpoint receiving events. See Payments. - Store tables migrated — run
supabase/migrations/0002_add_store_commerce.sql(adds the commerce columns plusorders/order_items). See Supabase Setup.
Step 1 — Create a Printful API token
In your Printful dashboard, open the API / Developers section — this is where you access API tokens.
From there, in the Printful Developers portal (developers.printful.com), create a private token. Under Access level, choose Store and select the store your products live in.
Set Access level → Store and select the store where your POD products actually live. A token scoped to the wrong store — or the default empty "Personal orders" store — makes product sync silently import 0 products and paid orders never reach fulfilment, with no error at token time.
Under Scopes, tick exactly what this integration uses:
- View and manage orders of the authorized store — submits the fulfilment order (a write). Required.
- View store products — reads your catalogue during sync (read-only is enough; View and manage store products also works). Required.
- View and manage store webhooks — only if you register the tracking webhook via the API in Step 5 (that
curluses this same token).
A token missing these still authenticates, then silently fails — no products sync, orders don't submit, or the webhook call is rejected. Scopes can't be changed after creation; you'd create a new token.
Add it to your environment:
# .env.local (and your host's env for production)
PRINTFUL_API_KEY=your_private_tokenStep 2 — Import your Printful catalogue
Open the admin Orders screen and click Sync Printful products. This pulls your Printful catalogue into the apps table as products (product_source: 'printful', matched by external_product_id), so they appear in your storefront with prices and variants.
Re-run the sync any time you add or change products in Printful.
Step 3 — Draft or auto-confirm?
By default, a paid order is created in Printful as a draft — you review and confirm each one in the Printful dashboard before it goes to production. That's a deliberate safety default so a misconfiguration can't rack up real charges.
When you're confident the flow is correct, set orders to submit for production automatically:
# Optional. Unset/false = orders land in Printful as drafts you confirm manually.
PRINTFUL_AUTO_CONFIRM=trueWith PRINTFUL_AUTO_CONFIRM=true, every paid order is sent straight to Printful for production and charges your Printful payment method. Test with drafts first, then enable it.
Step 4 — How a paid order reaches Printful
You don't call Printful yourself — the platform does it in the webhook. For a POD cart the flow is:
- Buyer checks out → native Stripe Checkout.
- Stripe fires
checkout.session.completed→ the verified webhook handler (metadata.type === 'store_order') records theorders/order_itemsrows and decrements stock atomically. - For any POD line items, it submits a Printful v2 order from the buyer's shipping address and stores
orders.fulfillment_order_id.
Each paid POD order shows its Printful fulfillment id right in your admin Orders screen (the Printful #… value in the Fulfillment column shown in Step 2). If Printful isn't configured — or a call fails — the order is still recorded with a fulfillment_error instead of being silently lost, so you can act on it.
On the Printful side, those same orders land in your Order list — here as drafts awaiting confirmation, because PRINTFUL_AUTO_CONFIRM is off:
Step 5 — Shipment tracking (Printful webhook)
To reflect shipment status and tracking numbers back onto your orders, register a webhook that points at your app.
There is no webhook screen in the Printful dashboard. You register the webhook by calling the Webhook API — one POST /webhooks request. (Printful's newer v2 adds signed webhooks; this boilerplate uses the classic endpoint plus a shared secret in the URL, below.)
1. Set the secret in your env first — the app rejects any call whose ?secret= doesn't match:
# Optional but recommended. When set, the webhook URL must carry ?secret=<this>.
PRINTFUL_WEBHOOK_SECRET=your_secret2. Register the webhook with a single API call. Note the ?secret= in the URL and the events the app actually handles (package_shipped, order_canceled, order_failed, order_refunded):
curl -X POST https://api.printful.com/webhooks \
-H "Authorization: Bearer $PRINTFUL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/api/webhooks/printful?secret=your_secret",
"types": ["package_shipped", "order_canceled", "order_failed", "order_refunded"]
}'You can verify it with GET https://api.printful.com/webhooks, or remove it with DELETE https://api.printful.com/webhooks.
Once wired, shipment updates land on the order (status, metadata.tracking_url / tracking_number), and the buyer sees a Track shipment link in their account's Orders tab.
Environment variables
| Variable | Required | Purpose |
|---|---|---|
PRINTFUL_API_KEY | Yes | Private token, scoped to the store holding your products. |
PRINTFUL_AUTO_CONFIRM | Optional | true submits paid orders to production; otherwise they're drafts. |
PRINTFUL_WEBHOOK_SECRET | Optional | Verifies the inbound Printful tracking webhook. |
STRIPE_WEBHOOK_SECRET | Yes | The paid-order → Printful flow runs inside the Stripe webhook. See Payments. |
Related
- Marketplace Config — turn on the store format and tune currency, cart, shipping and stock.
- Payments — Stripe products, the webhook, and local testing.
- Webhook Routes — how the raw-body webhook endpoints are wired.