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: {
code: 'classic-linen-epic-black-130-lb-cover',
key: 'classic-linen-epic-black-130-lb-cover',
name: 'Classic Linen Epic Black 130 lb. Cover',
hex: '#171618',
},
}),
});
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.
For registered paper stocks, StackFill maps substrate.code first and applies
the stock's base color and finish texture. key, exact name, and hex are
fallbacks in that order. Unknown stocks still render: a valid hex produces a
flat proof, and a missing hex uses StackFill's neutral proof stock.
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 · Text fitting and character limits
The template author configures Shrink to fit, Maximum width (pt), Minimum size (pt), and Character limit in the editor inspector. Existing templates can be saved with these settings; a replacement PDF is not needed. Fitting never enlarges, truncates, or wraps a single-line value. The text's baseline stays fixed. The same fitting rules apply to raster previews and final output.
Text alignment keeps a left, center, or right anchor within the authored text box. Maximum width is a separate shrink limit, not a new box origin. Split lines can retain a shared center/right edge while their pieces reflow. These settings apply to both preview and final PDF; existing templates need an alignment setting saved before their left-anchored text will center.
Use each field's max_length as the input's maxlength and show, for example,
"40 characters max". The cap counts UTF-16 code units, like HTML maxlength.
The API rejects longer input with HTTP 422 field_too_long and error.fields.
text_constraints lists every text box sharing the field, including other pages.
Use field.required for each input's required flag, not its position in the
form. The author controls this through Required in the inspector. A blank
required field returns 422 missing_required_field at save/completion; draft
create/patch remains available while the shopper is typing. Example/default
text is not a submitted value.
Click an expanded group's child row in Layers to edit just that object's constraints. Clicking the group heading selects the whole group for bulk edits.
After fetching a live preview, display any overflow warnings before enabling Add to cart:
const raw = previewResponse.headers.get('X-StackFill-Text-Warnings');
const warnings = raw ? JSON.parse(decodeURIComponent(raw)) : [];
// Each item: { field: 'company_name', code: 'text_overflow',
// min_font_size_pt: 6, max_width_pt: 108 }
// Resolve field IDs against the template's fields for customer-facing labels.
// Also require the latest edit to have saved and required fields to be filled.
addToCartButton.disabled = warnings.length > 0 || !latestEditSaved || !form.checkValidity();
Warnings accompany 200 PNG responses from /v1/embed/fills/{id}/preview and
/v1/previews, including cached PNGs. Keep the previous warnings on a 304.
The header is exposed through CORS and contains no entered text. Large lists
end with additional_text_overflow; keep checkout blocked until the list is
empty. A final PDF that still overflows at the minimum size is rejected with
HTTP 422 text_overflow. StackFill's hosted form and widget already show these
warnings; custom storefront forms must display the returned diagnostics.
Before attaching the fill, call POST /v1/embed/fills/{id}/complete and require
a successful response. It rechecks required values and text fit on the server
without producing or billing a final PDF. An unavailable validation renderer
returns 502/503, not approval. A concurrent edit returns 409 fill_changed.
The completed fill is frozen against browser edits.
On any failed create, patch, preview, or complete request, mark the preview
stale, show the actual error, and keep checkout disabled. Do not fall back to
the previous fill or a placeholder card. Match error.fields[].object_id or
error.fields[].field against field IDs/labels for inline messages. A later
successful response must correspond to the latest input revision before it
can clear the error. A 304 only confirms the state of its matching ETag.
Open a new embed session after changing a template. Existing sessions and fills
remain pinned to the revision on which they were started. Product mappings with
an explicit scene_revision must be advanced deliberately.
When syncing a template already cached in your platform, replace its cached
field schema from GET /v1/templates/{id}/fields as well as its revision.
Use the response's scene_revision alongside its data; don't keep old
limits/defaults/required flags under a new revision number. This endpoint and
GET /v1/templates/{id} return Cache-Control: no-store. This does not update
your platform's database cache automatically, or change an existing fill's
pinned artwork.
Authored colors versus selected inks
No ink palette (colors: []) preserves the scene's authored text colors.
Sending a palette deliberately replaces text colors: unassigned text uses
ink 1, and assignments selects other inks by field ID. This is not an
automatic "engraving means black" rule. Decorative paths/images are not
recolored by the text-ink override.
For a two-ink design, send both inks and assign the accent fields to ink 2. For a one-ink product, keep the purchased ink selection authoritative and generate thumbnails/proofs with that same palette and assignments. Do not silently remove the ink selection just to match a multicolor source thumbnail. The app's canonical URL is https://stackfill.com/app/.
7 · 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.