Developer Guide

The PromisePro delivery-promise widget is a small, self-contained script you drop onto a product page. It asks the PromisePro estimate API whether the item reaches the shopper's ZIP fast enough and—only then—shows a ticking "order within X → arrives DATE" promise. It's fail-closed: every other outcome (too slow, no route, an error, missing config) renders nothing, so the widget is invisible by default and only appears on a confirmed fast result.

It's platform-agnostic—Shopify, BigCommerce, WooCommerce, custom, or headless. Only the glue (how you fill the attributes and where the product id comes from) differs per platform.

Quick start

Add a root element to the product page:

<div id="promisepro-delivery" class="promisepro-delivery" data-spin="ABC123" data-threshold-days="3" data-locale="en-US" hidden></div>

Then load the hosted script once, from your environment's host:

<script src="https://api.spreetail.com/v1/delivery-estimate/widget.js" async></script>

That's it. The widget reads its configuration from the root element, detects the page quantity and the shopper's ZIP, calls the API, and renders the promise if the item arrives within data-threshold-days.

Configuration attributes

Set these data-* attributes on the root element.

AttributeRequiredDefaultDescription
data-spinYesYour Spreetail product id (SPIN) for this item/variant; sent to the API as item_id. Blank or missing → renders nothing.
data-threshold-daysYesShow the promise only when transit days are at or below this value. If omitted, the widget renders nothing (fails closed).
data-localeNoEnglishBCP-47 language (e.g. en-US, es, es-US). Drives copy and date formatting. See Localization.
hiddenRecommendedThe widget is invisible until a confirmed fast result, so start the element hidden to avoid any flash.
📘

There is no environment or color attribute

The widget is themed entirely with CSS variables (see Theming), and the environment is determined by which host you load the script from (see Environments). There is no data-env or data-accent.

Inputs that aren't attributes

These come from the page or the browser, not from data-*:

  • Quantity — read from the product page. The widget looks for the first of: input[name="quantity"], .quantity__input, input.quantity[type="number"], or any element with data-quantity-input. If your theme's quantity field matches none of these, add data-quantity-input to it. Defaults to 1 if none is found.
  • Destination ZIP — not supplied by you. The widget prompts the shopper for a 5-digit US ZIP and remembers it in localStorage (promisepro:zip). A "Change ZIP" control lets them update it.
  • API base — never configured on the page. The widget calls the estimate API at the same origin it was loaded from (the script's src). Loading the bundle from a given environment's host automatically targets that environment's API.

Populating data-spin and quantity

The widget itself has no catalog or platform knowledge: it reads the data-spin attribute and a quantity field on the page. How each gets populated is up to your storefront.

SPIN (data-spin)

The widget reads data-spin and sends it to the API as item_id. It does not read Shopify metafields (or any catalog) directly—your page template fills the attribute.

Shopify — the provided Liquid snippet maps the variant metafield promisepro.SPIN into the attribute at page-render time:

{%- assign promisepro_variant = product.selected_or_first_available_variant -%}
<div id="promisepro-delivery"
     data-spin="{{ promisepro_variant.metafields.promisepro.SPIN.value | escape }}"
     ...></div>

One-time setup:

  1. Define the metafield (per store, no app needed): namespace promisepro, key SPIN, owner type Product variant, storefront-readable. A helper script (create-metafield-definition.mjs) does this with a short-lived admin token.
  2. Populate per variant: Admin → Products → (variant) → Metafields → "PromisePro SPIN".

A variant with a blank or missing SPIN renders nothing (no API call)—expected fail-closed behavior, not a setup error.

⚠️

Variant switching isn't re-fetched (v1)

The SPIN is read by Liquid at page load for the selected / first-available variant. Switching variants in the browser does not update the promise until the page reloads.

Other platforms — fill data-spin from your own product context (a product custom field, data layer, or catalog attribute) using your platform's templating. The widget only needs the attribute populated.

Quantity

The widget reads the quantity from a field already on the product page—it doesn't render its own. It uses the first element matching, in order:

input[name="quantity"] · .quantity__input · input.quantity[type="number"] · [data-quantity-input]

It reads that element's value, coerces it to a positive integer (anything invalid → 1), and sends it as qty. If no field matches, qty defaults to 1. The widget re-runs the estimate when that field fires a change event, so changing quantity updates the promise.

📘

Custom quantity controls

If your theme's quantity field matches none of the selectors, add data-quantity-input to it. If a custom stepper updates the value without firing a native change event, the widget keeps the load-time quantity—make sure a change event fires (or dispatch one).


Theming

The widget renders inside a Shadow DOM, so your site's CSS can't reach or distort it—and its styles can't leak out. To brand it, set --promisepro-* CSS custom properties on the root element; they inherit across the shadow boundary.

VariableDefaultControls
--promisepro-accent#2fb37aDelivery date, ZIP, truck icon (your main brand color)
--promisepro-urgent-color#f6a623Countdown and clock icon
--promisepro-bg#1e2b3eCard background
--promisepro-color#f5f8fcMain text
--promisepro-muted-color#9fb1c7Labels ("Order within", "Deliver to")
--promisepro-error-color#ff9a9aZIP-error text
--promisepro-radius12pxCorner radius
--promisepro-margin0.75rem 0Outer margin
--promisepro-max-width360pxCard width
--promisepro-fontsystem stackFont family
--promisepro-font-size15pxFont size

Set them inline on the root:

<div id="promisepro-delivery" class="promisepro-delivery" data-spin="ABC123" data-threshold-days="3" style="--promisepro-accent:#0a7d55; --promisepro-bg:#ffffff; --promisepro-color:#111111; --promisepro-radius:8px; --promisepro-max-width:420px;" hidden></div>

…or from a stylesheet:

#promisepro-delivery { --promisepro-accent: #0a7d55; --promisepro-bg: #ffffff; --promisepro-color: #111111; --promisepro-font: "Inter", system-ui, sans-serif;}

The default palette is a dark card. For a light theme, set --promisepro-bg and --promisepro-color together, not just the accent.

Localization

Region is US-only (5-digit ZIP), but copy is localized by language. Pass the store language as data-locale; the widget selects a built-in translation table by the primary subtag (es-USes), falling back to English. English and Spanish ship today. Dates render in the same locale, and near-term deliveries use the localized "today"/"tomorrow".

⚠️

Locale is not auto-detected from the browser

If data-locale is omitted, copy defaults to English (the widget does not read navigator.language). Only the formatted date falls back to the browser's locale. For a fully localized widget, always pass data-locale.

Environments

The widget calls the estimate API at the same origin that served the bundle. To target an environment, load the script from that environment's host.

Production:

<script src="https://API_HOST_PROD/v1/delivery-estimate/widget.js" async></script>

Staging:

<script src="https://API_HOST_STAGING/v1/delivery-estimate/widget.js" async></script>

📘

Get the host from the PromisePro team

Use the per-environment host they provide. The bundle and the estimate API are always the same origin, so you never configure an API URL yourself.

The /v1/delivery-estimate endpoint is public (no auth or API key—a browser widget can't hold a secret) and rate-limited to 100 requests/second at the gateway. It returns permissive CORS headers, so cross-origin browser calls work out of the box.

Content Security Policy

If your storefront sets a CSP, allow the PromisePro host in both directives:

  • script-src — to load widget.js
  • connect-src — for the estimate fetch

A CSP miss fails closed: the widget simply stays hidden.

Single-page apps

On SPA or headless sites where the product DOM is swapped in after navigation, re-mount the widget after each render:

window.PromisePro.init();

It's idempotent—safe to call repeatedly; already-initialized roots are skipped.

What the shopper sees

OutcomeResult
Fast enough (transit at or below threshold), in stock, valid ZIPThe promise: "Order within HH:MM:SS → arrives DATE", counting down live
No ZIP entered yetA compact "enter your ZIP" prompt
Too slow, out of stock, no route, API error, or any non-200Nothing (the element stays hidden)

Troubleshooting

SymptomCause / fix
Blank pageA self-closing <script src="…" />. Use <script src="…" async></script>.
Widget never appearsIt's fail-closed. Check: data-spin is set and valid; data-threshold-days is set; a quantity input is detectable (or add data-quantity-input); a ZIP was entered; the API returns a fast-enough 200.
Labels are English on a non-English storePass data-locale (copy isn't auto-detected from the browser).

Full example

A product page needs three things: a quantity input (your theme's), the widget root, and the script. Theming via style is optional.

<input name="quantity" type="number" value="1" min="1" /><div id="promisepro-delivery" class="promisepro-delivery" data-spin="ABC123" data-threshold-days="3" data-locale="en-US" style="--promisepro-accent:#0a7d55" hidden></div><script src="https://api.spreetail.com/v1/delivery-estimate/widget.js" async></script>