Headless & 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.
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:
| 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)
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>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-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
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:
| 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
- 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
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.
