API guide

Storefront integration

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.