Home
Headless & Hydrogen

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.

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.

Note

Beta. Both install paths are live: npm install @artossoftware/stoq-sdk, or the script tag below (served from jsDelivr's CDN, pinned to the current major). Feedback: support@stoqapp.com.

There are three ways to integrate, from least to most code:

PathForYou write
Script tagAny headless stack, no build stepOne <script> tag + a few lines of JS
npm + React hooksHydrogen / React 18+ storefrontsComponents using our hooks
Raw APIsAnything else / full controlYour own client against our metafields + the signup endpoint

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>
Note

data-storefront-token is your public Storefront API access token — the same one your headless store already uses (public tokens are designed to be exposed in browser code; never use a private Storefront API token here). With it, the SDK reads everything from Shopify's edge-cached Storefront API — fast, close to your shoppers, and as fresh as Shopify itself.

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

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:

ExportWhat 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

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

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

The SDK fires the same custom events as the theme embed (on window), so analytics and integrations behave identically:

EventWhen
stoq:loadedSDK initialized
stoq:restock-modal:opened / closedModal lifecycle
stoq:restock-modal:submittedSignup created (modal, inline form, or createSignup)

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

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

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.