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.
| Attribute | Required | Default | Description |
|---|---|---|---|
data-spin | Yes | — | Your Spreetail product id (SPIN) for this item/variant; sent to the API as item_id. Blank or missing → renders nothing. |
data-threshold-days | Yes | — | Show the promise only when transit days are at or below this value. If omitted, the widget renders nothing (fails closed). |
data-locale | No | English | BCP-47 language (e.g. en-US, es, es-US). Drives copy and date formatting. See Localization. |
hidden | Recommended | — | The widget is invisible until a confirmed fast result, so start the element hidden to avoid any flash. |
There is no environment or color attributeThe 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-envordata-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 withdata-quantity-input. If your theme's quantity field matches none of these, adddata-quantity-inputto it. Defaults to1if 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)
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:
- Define the metafield (per store, no app needed): namespace
promisepro, keySPIN, owner type Product variant, storefront-readable. A helper script (create-metafield-definition.mjs) does this with a short-lived admin token. - 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 controlsIf your theme's quantity field matches none of the selectors, add
data-quantity-inputto it. If a custom stepper updates the value without firing a nativechangeevent, the widget keeps the load-time quantity—make sure achangeevent 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.
| Variable | Default | Controls |
|---|---|---|
--promisepro-accent | #2fb37a | Delivery date, ZIP, truck icon (your main brand color) |
--promisepro-urgent-color | #f6a623 | Countdown and clock icon |
--promisepro-bg | #1e2b3e | Card background |
--promisepro-color | #f5f8fc | Main text |
--promisepro-muted-color | #9fb1c7 | Labels ("Order within", "Deliver to") |
--promisepro-error-color | #ff9a9a | ZIP-error text |
--promisepro-radius | 12px | Corner radius |
--promisepro-margin | 0.75rem 0 | Outer margin |
--promisepro-max-width | 360px | Card width |
--promisepro-font | system stack | Font family |
--promisepro-font-size | 15px | Font 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-US → es), 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 browserIf
data-localeis omitted, copy defaults to English (the widget does not readnavigator.language). Only the formatted date falls back to the browser's locale. For a fully localized widget, always passdata-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 teamUse 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 loadwidget.jsconnect-src— for the estimatefetch
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
| Outcome | Result |
|---|---|
| Fast enough (transit at or below threshold), in stock, valid ZIP | The promise: "Order within HH:MM:SS → arrives DATE", counting down live |
| No ZIP entered yet | A compact "enter your ZIP" prompt |
| Too slow, out of stock, no route, API error, or any non-200 | Nothing (the element stays hidden) |
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Blank page | A self-closing <script src="…" />. Use <script src="…" async></script>. |
| Widget never appears | It'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 store | Pass 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>
