Skip to content

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

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.

Terminal window
npm install @artossoftware/stoq-sdk

Wrap 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.

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.

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)
  • 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 set country in the config).
  • Gate notify-me UI on your own availability data (!variant.availableForSale) plus client.signupsEnabled.
  • Market-scoped offers need market in the config. An offer restricted to specific Shopify Markets only applies when you pass market (the theme embed reads it from localization.market.id; the SDK can’t infer it). Without it, every variant on such an offer reports isPreorder: 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 a sellingPlanId; the SDK marks the line with attributes (_stoq_shopify_selling_plan_id + Purchase type line-item properties, the same marker the theme embed writes). Pass the line to cartLinesAdd untouched.
  • If nothing renders, check useStoq().status / .error (or the console — the Provider logs a console.error when init fails). Hooks and components return null on 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.

Everything the SDK does is built on public surfaces you can use directly:

  • Back-in-stock signups — POST /api/v1/intents.json is 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, variant shipping_text, preorder_max_count, …) are exposed with PUBLIC_READ storefront 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.

Was this page helpful?

Still stuck? Ask Quinn or chat with support