Developer Guide
The PromisePro delivery-promise widget is a self-contained <script> for product pages. It shows "order within [countdown] → arrives [date]" — but only when delivery is fast enough. On any failure it renders nothing (fail-closed), so a shopper never sees a promise the network can't keep.
The widget is platform-agnostic: it reads data-* attributes on a root element and calls the PromisePro estimate API at the same origin that served the bundle. Nothing in it is tied to a specific storefront platform.
Installation
Add two things to a product page:
<div id="promisepro-delivery"
class="promisepro-delivery"
data-spin="YOUR_ITEM_ID"
data-threshold-days="3"
data-locale="en-US"
hidden></div>
<script src="https://api.spreetail.com/api/v1/delivery-estimate/widget.js" async></script>The widget reads its configuration from data-* attributes, detects the page quantity input, prompts the shopper for a ZIP, calls the API, and renders the promise if delivery is fast enough.
Configuration
| Attribute | Required | Default | Description |
|---|---|---|---|
data-spin | Yes* | — | Product id (SPIN) sent to the API as item_id. Blank → renders nothing. *Not required when data-spin-list is set (bundle mode). |
data-spin-list | No | — | JSON array of { spin, qty } bundle components. When non-empty, the widget estimates the whole bundle via a single POST (see Multi-item bundles) instead of using data-spin. spin may be a number or string; qty is optional (defaults to 1). Malformed or blank → falls back to single-item mode. |
data-threshold-days | Yes | — | Show only when transit days ≤ this value. Absent → renders nothing. |
data-cutoff-offset | No | 20 | Minutes trimmed off the displayed order-by cutoff (safety buffer). |
data-locale | No | English | BCP-47 language (e.g. en-US, es). Drives copy and date formatting. |
hidden | Recommended | — | Start hidden; the widget reveals itself only on a confirmed fast result (prevents a flash). |
data-show-default-delivery-window | Optional | Enabled | When the item is slower than data-threshold-days but still under 7 transit days, shows a "Get it between X–Y days" window instead of hiding. Lower bound = threshold− 1 (min 1); upper bound = the API's transit_days. Set to "false" to restore hide-on-slow behavior. Items ≥7 days, or any failure, still render nothing. |
Implicit inputs (not attributes)
- Quantity — read from the page. The widget scans for
input[name="quantity"],.quantity__input,input.quantity[type="number"], or[data-quantity-input]. Defaults to 1 if none found; re-fetches on the input'schangeevent. - ZIP — prompted from the shopper and cached in
localStorage(promisepro:zip). - API base — the origin that served
widget.js. Never configured on the page. To target an environment, load the script from that environment's host.
Multi-item bundles
For a single listing that ships as several components, set data-spin-list to a JSON array of { spin, qty } instead of (or in addition to) data-spin. When that list is non-empty, the widget switches to bundle mode: it estimates all components together with one POST /v1/delivery-estimate and renders a single combined promise.
<div id="promisepro-delivery"
class="promisepro-delivery"
data-spin-list='[{"spin":427936,"qty":2},{"spin":427937}]'
data-threshold-days="3"
data-locale="en-US"
hidden></div>
<script src="https://api.spreetail.com/api/v1/delivery-estimate/widget.js" async></script>Behavior in bundle mode:
- Quantity multiplies the whole bundle. The page quantity is applied to every component — ordering N bundles requests
N ×each component'sqty. - One promise, shared date. The response is the same
{ cutoff_time, estimated_delivery_date, transit_days }estimate as single-item mode, computed from the earliest day every component is orderable together. - All-or-nothing, still fail-closed. If any component can't be promised (out of stock, no route, unknown), the widget renders nothing — never a partial promise.
data-spin-listwins overdata-spin. A non-empty list uses the bundlePOST;data-spinis ignored. Each component'sspinmay be a number or string, andqtyis optional (defaults to 1).- Malformed → single-item fallback. If
data-spin-listisn't valid JSON or resolves to an empty array, the widget falls back to single-item mode usingdata-spin.
Theming
The widget renders in a Shadow DOM, so storefront CSS cannot reach its internals. Its default palette is brand-agnostic and adapts to the shopper: with no overrides it follows the shopper's prefers-color-scheme — a light card on a light system theme, a dark card on a dark one. Brand it by setting --promisepro-* CSS custom properties on the root element; they inherit across the shadow boundary and win over both auto defaults.
| Variable | Light default | Dark default | Controls |
|---|---|---|---|
--promisepro-accent | #0f766e | #34d399 | Delivery date, ZIP, truck icon |
--promisepro-accent-contrast | #ffffff | #06281b | Text/icon sitting on the accent (submit button) |
--promisepro-urgent-color | #b45309 | #fbbf24 | Countdown + clock |
--promisepro-bg | #ffffff | #12161d | Card background |
--promisepro-color | #14181f | #f3f4f6 | Main text |
--promisepro-muted-color | #5a6572 | #9aa4b2 | Labels |
--promisepro-error-color | #b42318 | #f87171 | Error text |
--promisepro-border-color | #e2e6ea | rgba(255,255,255,0.12) | Card border |
--promisepro-input-bg | #f5f7f9 | rgba(255,255,255,0.08) | ZIP input fill |
--promisepro-input-border | #cfd6dd | rgba(255,255,255,0.25) | ZIP input border |
--promisepro-radius | 12px | Corner radius | |
--promisepro-margin | 0.75rem 0 | Outer margin | |
--promisepro-max-width | 400px | Card width | |
--promisepro-font | system stack | Font family | |
--promisepro-font-size | 16px | Font size |
The default palette meets WCAG AA contrast in both themes. If you set a custom --promisepro-accent, verify it against both a light and a dark card background.
Pinning a theme
Each variable falls back independently, so to pin a fixed look you must set the surface colors together — --promisepro-bg and --promisepro-color (plus --promisepro-accent). Setting only one (e.g. just the background) leaves the others following the shopper's system theme and can produce an unreadable mix.
<!-- Fixed light card with a brand accent -->
<div id="promisepro-delivery" class="promisepro-delivery"
data-spin="ABC123" data-threshold-days="3"
style="--promisepro-accent:#0a7d55; --promisepro-bg:#ffffff; --promisepro-color:#14181f; --promisepro-radius:8px;"
hidden></div>Localization
Pass the store language as data-locale (BCP-47). The widget picks a built-in translation table by primary subtag (es-US → es), falling back to English. English and Spanish ship today; dates render in the same locale. Locale is not auto-detected from the browser — if data-locale is omitted, copy defaults to English.
SPA / client-rendered sites
If the product DOM is swapped after page load, re-mount with:
window.PromisePro.init();It is idempotent — safe to call on every route change.
Deployment notes
- CSP: allow the PromisePro host in both
script-src(to load the bundle) andconnect-src(the widget calls the estimate API at that same origin). - API: the estimate endpoint is public, CORS-enabled, and rate-limited. It answers both the single-item
GETand the bundlePOST(whose JSON body triggers a CORS preflight, which the endpoint handles).
What the shopper sees
| Outcome | Result |
|---|---|
| Fast enough, in stock, valid ZIP | Countdown + delivery date |
| No ZIP yet | "Enter your ZIP" prompt |
| Too slow / out of stock / no route / API error | Nothing (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-cutoff-offset="20"
data-locale="en-US"
style="--promisepro-accent:#0a7d55"
hidden></div>
<script src="https://api.spreetail.com/api/v1/delivery-estimate/widget.js" async></script>Updated 10 days ago
