Headless & Hydrogen
Run STOQ preorders and back-in-stock on a headless storefront with the Storefront SDK — a script tag for any stack, React hooks for Hydrogen.
BetaThe SDK is in public beta: endpoints, fields and behaviour can change before general availability. See the changelog
Last updated
STOQ’s storefront widgets ship as a Shopify theme app embed, which doesn’t load on headless storefronts (Hydrogen, Next.js Commerce, custom builds). The STOQ Storefront SDK brings the same capabilities to those stacks: preorder state per variant, cart lines with the right selling plan attached, and back-in-stock signups — including the notify-me modal.
📦 npm: @artossoftware/stoq-sdk — framework-free, fully typed, SSR-safe (nothing touches window/document at import), zero runtime dependencies. Setup is two fields: your shop domain and its public Storefront API access token (the same token your headless store already uses). It loads the merchant’s STOQ config once at init and fetches per-variant availability on demand (batched + cached) from your shop’s own Storefront API on Shopify’s edge.
There are three ways to integrate, from least to most code:
| Path | For | You write |
|---|---|---|
| Script tag | Any headless stack, no build step | One <script> tag + a few lines of JS |
| npm + React hooks | Hydrogen / React 18+ storefronts | Components using our hooks |
| Raw APIs | Anything else / full control | Your own client against our metafields + the signup endpoint |
1. Script tag (any stack)
Section titled “1. Script tag (any stack)”Add one tag to your <head>. It identifies your shop by domain — no credentials go in your HTML, and your admin API key is never used in the storefront:
<script src="https://cdn.jsdelivr.net/npm/@artossoftware/stoq-sdk@0/dist/stoq.min.js" data-shop="your-shop.myshopify.com" data-storefront-token="your-public-storefront-api-token" defer></script>The script defines a global Stoq, initializes automatically, and fires stoq:loaded when ready:
<script> window.addEventListener('stoq:loaded', async () => { // Ask by id — the SDK fetches the variant's availability itself // (batched + cached). If you already have the variant object, pass // { id, availableForSale, currentlyNotInStock } to skip that lookup. const state = await Stoq.client.getVariantState(42424242424242)
if (state.isPreorder) { // Show your preorder UI; state.shippingText has the estimated-shipping copy. // Add to cart with state.sellingPlanGid (see "Carts" below). } else if (Stoq.client.signupsEnabled /* && your data says it's sold out */) { // One call opens the STOQ notify-me modal — form, validation, // success states and styling come from your STOQ design settings: Stoq.openModal({ variantId: 42424242424242, productTitle: 'Classic Tee' }) } })</script>Optional attributes: data-market (Shopify Market id for market-scoped offers) and data-locale. Without data-shop the script only defines the global — call Stoq.init({...}) yourself.
The modal can also render as an inline form inside any element you supply: Stoq.openInlineForm({ container: '#notify-me', variantId }). Texts and colors default to your dashboard design settings and accept per-call overrides.
2. React / Hydrogen
Section titled “2. React / Hydrogen”npm install @artossoftware/stoq-sdkWrap your app (or just the product route) in the provider, then use the hooks. Pass the public Storefront API token your Hydrogen app already ships with — the PUBLIC_STOREFRONT_API_TOKEN env var (the same value createStorefrontClient uses as publicStorefrontToken):
export default function App() { return ( <StoqProvider config={{ shop: 'your-shop.myshopify.com', storefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN, }} > <Outlet /> </StoqProvider> )}A complete add-to-cart button that handles preorders automatically — the line input from useStoqCartLine already carries sellingPlanId when (and only when) the variant should be sold as a preorder, so it passes straight into Hydrogen’s CartForm:
function BuyBox({ variant }) { // Pass the variant object itself (include availableForSale and // currentlyNotInStock in your variant fragment) — its availability fields // decide preorder state. GIDs are accepted directly. const state = useStoqVariant(variant) const line = useStoqCartLine(variant, 1)
// The SDK initializes in an effect (SSR-safe), so the server render and // first client paint don't have its answer yet. Don't return null — fall // back to a plain add-to-cart line so the buy box is never empty (this // also covers a misconfigured Provider, which console.errors and stays // in status 'error'). const lines = [line ?? { merchandiseId: variant.id, quantity: 1 }]
if (!variant.availableForSale && !state?.isPreorder) { return <StoqNotifyMeButton variantId={variant.id} productId={variant.product.id} /> }
return ( <CartForm route="/cart" action={CartForm.ACTIONS.LinesAdd} inputs={{ lines }}> <button type="submit">{state?.isPreorder ? 'Preorder now' : 'Add to cart'}</button> {state?.isPreorder && state.shippingText ? <p>{state.shippingText}</p> : null} </CartForm> )}Adding from a callback — <StoqPreorderButton onAddToCart> (which runs the payment-option modal for you), or after openPreorderModal resolves — means posting to the /cart action yourself. toCartFormInput(line) builds the { cartFormInput } payload CartForm expects:
const fetcher = useFetcher()<StoqPreorderButton variantId={variant} productId={variant.product.id} onAddToCart={(line) => fetcher.submit(toCartFormInput(line), { method: 'post', action: '/cart' })}/>Available from @artossoftware/stoq-sdk/react:
| Export | What it does |
|---|---|
useStoqVariant(variant) |
{ isPreorder, sellingPlanId, shippingText, maxCount, remainingCount } — null while loading |
useStoqCartLine(variant, qty?) |
Storefront Cart API CartLineInput — carries sellingPlanId (selling-plan offers) or attributes (property-based offers) only when preorderable |
useStoqPreorder(variant) |
{ state, button, options, openModal, resolveLine } — merchant CTA copy, payment options, the preorder modal, the final line |
useStoqSignup() |
{ submit, submitting, result, error } for building your own notify-me UI |
useStoq() |
Raw { client, status, error, refresh } access — check status === 'error' / error if nothing renders |
toCartFormInput(line) |
Builds the { cartFormInput } payload for Hydrogen’s /cart action when adding from a callback |
<StoqPreorderButton> |
Merchant-configured preorder CTA → payment-option modal → onAddToCart(line); renders only when preorderable |
<StoqPreorderBadge> |
Renders children (default “Preorder”) only when the variant is preorderable |
<StoqNotifyMeButton> |
Button → inline email form → signup; minimal overrideable default styling (stoq-* classnames + --stoq-* CSS variables) |
Everything is SSR-safe — importing never touches window, and components render nothing on the server.
The vanilla core (import { init } from '@artossoftware/stoq-sdk') is the same engine without React — use it from Vue, Svelte, or plain JS with the identical getVariantState / cartLineFor / createSignup API.
How data loads: all reads come from Shopify
Section titled “How data loads: all reads come from Shopify”init({ shop, storefrontToken }) makes ONE GraphQL call to your shop’s own edge-cached Storefront API for the merchant’s STOQ configuration (offers, texts, limits), and per-variant availability is looked up on demand when you ask about a variant (batched — concurrent lookups share one request — and cached for 60 seconds). There are no catalog-wide inventory lists to download, and reads are served from Shopify’s edge — fast and as fresh as Shopify itself.
Already have the variant object from your own product query? Pass it ({ id, availableForSale, currentlyNotInStock }) instead of a bare id and the availability lookup is skipped — your (possibly @inContext-localized) data is used verbatim.
Signups post to STOQ (it’s a write into our system) — that’s the SDK’s only STOQ call, it only fires on actual signups, and like all our APIs it is subject to rate limits.
Carts: how preorders attach
Section titled “Carts: how preorders attach”On headless storefronts, carts are Storefront API carts, and preorders work by attaching a selling plan to the line. The SDK hands you the finished line input:
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { id totalQuantity } }}{ "cartId": "gid://shopify/Cart/...", "lines": [{ "merchandiseId": "gid://shopify/ProductVariant/42424242424242", "sellingPlanId": "gid://shopify/SellingPlan/3232323232", "quantity": 1 }]}That’s all a preorder is at checkout time — STOQ’s backend takes over from the order webhook onward (payment scheduling, fulfillment holds, tagging).
One exception: offers configured as property-based (use_shopify_selling_plan: false) don’t use a Shopify selling plan at all. For those the SDK’s line carries attributes instead of sellingPlanId:
{ "merchandiseId": "gid://shopify/ProductVariant/42424242424242", "quantity": 1, "attributes": [ { "key": "_stoq_shopify_selling_plan_id", "value": "3232323232" }, { "key": "Purchase type", "value": "Preorder" } ]}Those are the same two line-item properties the theme app embed writes; STOQ recognizes the order as a preorder from them. You don’t need to know which kind of offer a variant is on — cartLineFor / useStoqCartLine emit the right marker — just pass the whole line through.
Events
Section titled “Events”The SDK fires the same custom events as the theme embed (on window), so analytics and integrations behave identically:
| Event | When |
|---|---|
stoq:loaded |
SDK initialized |
stoq:restock-modal:opened / closed |
Modal lifecycle |
stoq:restock-modal:submitted |
Signup created (modal, inline form, or createSignup) |
Behavior notes
Section titled “Behavior notes”- Bare ids work everywhere — the SDK fetches availability itself. Passing your variant object (
availableForSale,currentlyNotInStock) skips that lookup and uses your@inContext-localized data verbatim, which is the most precise option for multi-market stores (or setcountryin the config). - Gate notify-me UI on your own availability data (
!variant.availableForSale) plusclient.signupsEnabled. - Market-scoped offers need
marketin the config. An offer restricted to specific Shopify Markets only applies when you passmarket(the theme embed reads it fromlocalization.market.id; the SDK can’t infer it). Without it, every variant on such an offer reportsisPreorder: false— a shop whose offers are all market-scoped looks like it has no preorders at all. Global offers work without it. - Property-based offers (
use_shopify_selling_plan: false) never get asellingPlanId; the SDK marks the line withattributes(_stoq_shopify_selling_plan_id+Purchase typeline-item properties, the same marker the theme embed writes). Pass the line tocartLinesAdduntouched. - If nothing renders, check
useStoq().status/.error(or the console — the Provider logs aconsole.errorwheninitfails). Hooks and components returnnullon error exactly as they do while loading, so a wrong token otherwise looks like “no preorder plan”. - Offer configuration can be briefly stale. The metafields STOQ writes are re-synced on merchant saves, Shopify’s edge may cache reads, and the SDK session-caches for 5 minutes per shop+market — call
refresh()after operations that change offers. Availability is fresher: a 60-second cache, or exactly your own data when you pass the variant object.
3. No SDK: raw APIs & metafields
Section titled “3. No SDK: raw APIs & metafields”Everything the SDK does is built on public surfaces you can use directly:
- Back-in-stock signups —
POST /api/v1/intents.jsonis CORS-open and unauthenticated (shop identified by domain): see Create an Intent. - Variant/offer state via Storefront API metafields — STOQ’s shop- and variant-level metafields (namespace
restockrocket_production:cachedSellingPlans,preorder_variant_ids, variantshipping_text,preorder_max_count, …) are exposed withPUBLIC_READstorefront access, so any Storefront API client — including a Hydrogen loader running server-side — can read preorder state with zero STOQ API calls:
query ProductPreorderState($handle: String!) { shop { sellingPlans: metafield(namespace: "restockrocket_production", key: "cachedSellingPlans") { value } } product(handle: $handle) { variants(first: 50) { nodes { id shippingText: metafield(namespace: "restockrocket_production", key: "shipping_text") { value } preorderMaxCount: metafield(namespace: "restockrocket_production", key: "preorder_max_count") { value } } } }}- Admin operations (creating offers, releasing fulfillments, reports) — the v2 API, server-side only.
Never ship X-Auth-Token (your admin API key) to the client. It grants full read/write access to your STOQ data. The SDK never uses it — storefront calls are identified by shop domain only.
Thanks for letting us know.
Still stuck? Ask Quinn or chat with support