Storefront integration
Sell personalized print from any storefront — Odoo, WooCommerce, Shopify, or a
bespoke cart — with the same five touchpoints. StackFill stays
platform-neutral: your product, variant, cart, and order references are opaque
strings it never interprets, the widget speaks postMessage, and every
server call is a Bearer-authed /v1 request. An integration is glue, not
business logic — if you find yourself encoding print rules in the storefront,
that logic belongs in StackFill instead.
The contract at a glance
| # | Touchpoint | Direction | Mechanism |
|---|---|---|---|
| 1 | Personalize | browser → StackFill | Load widget/v1.js; open with a publishable key + your product/variant ids. |
| 2 | Attach to cart | browser → your platform | Catch stackfill:saved; store fill.id on the cart line. |
| 3 | Finalize on order | your server → StackFill | POST /v1/fills/{id}/finalize with your external_order_id. |
| 4 | Fulfill | your server → StackFill | GET /v1/renders?external_order_id=… → signed production-PDF URL. |
| 5 | Live custom preview | browser ← StackFill | GET /v1/embed/fills/{id}/preview for a settled PNG in your own editor. |
| 6 | Listing imagery | your platform ← StackFill | POST /v1/previews for product/listing thumbnails. |
That is the whole surface. A WooCommerce plugin, a Shopify app, and the Odoo
stackfill_connector module all implement the same platform-neutral steps — nothing
in StackFill core is platform-specific.
1 · Personalize (browser)
Embed the widget on the product page. The publishable key is origin-allowlisted and safe to ship to the browser; the product and variant ids are whatever your platform calls them — StackFill resolves them through product mappings.
<script src="https://stackfill.com/widget/v1.js" async></script>
<button
data-stackfill-key="pk_live_…"
data-stackfill-product="SKU-OR-PRODUCT-ID"
data-stackfill-variant="VARIANT-ID"
data-stackfill-cart="CART-OR-SESSION-ID">
Personalize
</button>
2 · Attach to cart (browser)
When the shopper saves, the widget emits a stackfill:saved message whose
payload carries both the embed_session and the fill. Take fill.id and
write it onto the cart line however your platform stores line metadata —
a hidden input, a cart attribute, a line-item property.
window.addEventListener('message', (e) => {
if (e.data?.type !== 'stackfill:saved') return;
const fillId = e.data.fill.id;
// e.g. Odoo: POST /shop/cart/update (form-encoded, with csrf_token),
// writing fillId into the order-line attributes.
attachToCartLine(fillId, e.data.fill.preview_url);
});
See Widget events for the full payload.
3 · Finalize on order (server)
On order confirmation, finalize the fill from your server with your order
reference. This is the platform-neutral fulfillment primitive — StackFill
records external_order_id and returns the production render id.
curl https://api.stackfill.com/v1/fills/FILL_ID/finalize \
-H "Authorization: Bearer stackfill_live_sk_…" \
-H "Content-Type: application/json" \
-d '{ "external_order_id": "YOUR-ORDER-REF" }'
Store the returned render_id on the order.
4 · Fulfill (server)
Resolve the production PDF by your own order reference — no StackFill ids to persist beyond what you chose to keep:
curl "https://api.stackfill.com/v1/renders?external_order_id=YOUR-ORDER-REF" \
-H "Authorization: Bearer stackfill_live_sk_…"
Each render carries a one-hour signed_url to the press-ready PDF. On a
refund or cancellation, flip the fill's status through the fill-status
endpoint — your adapter decides what a "credit note" or "cancel" means; the
contract only needs the status change.
5 · Live preview in a custom editor (browser)
Use the widget when StackFill should own the personalization UI. If your store
owns the form and swatch layout, create an embed fill, PATCH its current state,
then request the canonical raster directly. Use fields[].id from the template
fields endpoint as the keys in values and assignments; human labels remain
accepted for existing integrations.
await fetch(`https://api.stackfill.com/v1/embed/fills/${fillId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-StackFill-Client-Secret': clientSecret,
},
body: JSON.stringify({
values: { fld_name: 'Eleanor Ashcroft', fld_title: 'Director' },
colors: [
{ index: 1, key: 'black', name: 'Black', hex: '#111111' },
{ index: 2, key: 'merlot', name: 'Merlot', hex: '#6e1f2e' },
],
assignments: { fld_name: 1, fld_title: 2 },
substrate: { key: 'navy', name: 'Navy', hex: '#2a3550' },
}),
});
const preview = await fetch(
`https://api.stackfill.com/v1/embed/fills/${fillId}/preview?w=880&dpr=2`,
{ headers: { 'X-StackFill-Client-Secret': clientSecret } },
);
const imageUrl = URL.createObjectURL(await preview.blob());
document.querySelector('[data-design-preview]').src = imageUrl;
The preview response is image/png. page is 0-based; w is CSS pixels;
physical width is min(w × dpr, 2560). StackFill exposes a strong ETag,
X-StackFill-Content-Hash, physical dimensions, and X-StackFill-Cache.
Identical fill state plus render parameters is served from a content-addressed
cache. The preview budget is 120 requests per embed-session secret per 60
seconds; debounce text input before PATCH + preview.
Use X-StackFill-Client-Secret so the credential cannot enter browser history,
copied image URLs, referrer headers, or ordinary proxy URL logs. The legacy
query form remains supported for compatibility, but new integrations should
fetch the image with the header and display a local blob URL. A fill from
another session returns 404.
6 · Listing imagery (platform)
Pre-generate product and listing thumbnails from the same engine that prints the card, so what a shopper sees can't drift from what ships:
curl https://api.stackfill.com/v1/previews \
-H "Authorization: Bearer stackfill_live_sk_…" \
-H "Content-Type: application/json" \
-d '{
"template_id": "tpl_…",
"page": 0,
"values": { "fld_name": "Sample Name" },
"assignments": { "fld_name": 1 }
}' --output listing.png
Store the PNG as the product image. Refresh it from the render.succeeded /
batch.completed webhooks if the template changes.
The rule for adapters
An adapter is glue only: no rendering, no print rules, no StackFill semantics. If a platform genuinely needs something these touchpoints don't cover, extend the contract — platform-neutrally, so every adapter benefits — rather than special-casing one storefront in StackFill core.