Every public endpoint lives under /api/storefront/v1/. Most sites never call them by hand: the storefront widget does it for them. Reach for this page when a site builds its own UI, or when a build pipeline reads the catalog server side.

Endpoints

A browser request must come from an origin authorized in Storefront: live, local, or staging.

GET https://wiregum.com/api/storefront/v1/products?organization=workspace-slug&products=wg_product_ref
{
  "data": [
    {
      "id": "wg_prod_9f2c",
      "slug": "tote-bag",
      "name": "Tote bag",
      "images": ["https://..."],
      "defaultVariantId": "wg_var_31a8",
      "variants": [
        {
          "id": "wg_var_31a8",
          "sku": "TOTE-BLK",
          "title": "Black",
          "optionValues": { "Colour": "Black" },
          "unitAmount": 2500,
          "currency": "eur",
          "inStock": true
        }
      ]
    }
  ],
  "page": { "limit": 50, "hasMore": true, "nextCursor": "wg_prod_9f2c" },
  "shippingCountries": ["IT", "FR"],
  "ui": { "addToCart": "Aggiungi al carrello" }
}

A product carries one public identifier, id, plus its slug. Either can be used as a product reference in markup and in the products query parameter. Prefer id when the integration has to survive a slug change.

Variants report inStock, not a count. How many units are left is the merchant's own number, and a public storefront is readable by anyone including a competitor, so the exact figure is only returned to a caller holding the workspace API key, as stock. Overselling is prevented at checkout regardless: a cart that no longer fits the warehouse answers insufficient_stock.

Static site generators fetching products at build time, and server-rendered pages, send no browser origin. Generate a read-only API key in Storefront and pass it as a bearer token. The key only reads products: checkout sessions still require a browser request from an authorized domain. Keyed responses are never cached at the edge, since they are not the same answer everyone else gets.

The key is a secret, and the wg_sk_ prefix says so. It returns stock counts an authorized domain never sees, so it belongs in a server environment variable or a build secret and never in anything the browser downloads. Keys generated before this prefix existed start with wg_pk_; they still work and mean exactly the same thing, and rotating in Storefront issues the new shape.

# Build pipelines and server-rendered pages have no browser origin:
# authenticate with the workspace API key from WireGum Storefront instead.
curl "https://wiregum.com/api/storefront/v1/products?products=wg_product_ref" \
  -H "Authorization: Bearer wg_sk_your_key"

Paging the catalog

Listing requests return limit products, 50 by default and 100 at most, newest first. page.hasMore says whether the catalog was cut and page.nextCursor says where to carry on. A request that names products with product or products is a lookup rather than a listing and is never paged.

# Walk a catalog larger than one page.
GET /api/storefront/v1/products?organization=workspace-slug&limit=100
GET /api/storefront/v1/products?organization=workspace-slug&limit=100&cursor=wg_prod_9f2c

# Stop when page.hasMore is false.

Rate limits

Both endpoints are limited per IP over a one minute window: 120 requests for products, 10 for checkout. Only requests that miss the edge cache count. Going over answers rate_limited with a Retry-After header, which cross-origin JavaScript is allowed to read.

POST https://wiregum.com/api/storefront/v1/checkout
Content-Type: application/json

{
  "organization": "workspace-slug",
  "locale": "it",
  "shippingCountry": "IT",
  "successPath": "/thank-you",
  "cancelPath": "/cart",
  "items": [
    { "variantId": "wg_variant_ref", "quantity": 1 }
  ]
}

successPath and cancelPath are resolved against the storefront origin that created the checkout. Sites that need full control can send absolute successUrl and cancelUrl values instead, as long as they point to a domain authorized in WireGum Storefront. Stripe replaces a literal {CHECKOUT_SESSION_ID} placeholder in the success URL.

Errors

Every failed request answers with a code and an HTTP status. The code is the part to write logic against: it never changes meaning and never changes status. The error string next to it is a readable hint for logs and network tabs, and its wording can change at any time.

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "code": "domain_not_authorized",
  "error": "This storefront domain is not authorized in WireGum settings."
}
const response = await fetch(url);
const payload = await response.json();

if (!response.ok) {
  // Branch on the code, never on the sentence.
  if (payload.code === "domain_not_authorized") {
    showSetupHelp();
  }

  throw new Error(payload.error);
}
CodeStatusMeaning
invalid_request422The payload does not match the schema. The message names the fields at fault.
invalid_api_key401No workspace has this storefront key. Generate a new one in Storefront.
api_key_organization_mismatch403The key is valid but belongs to a different workspace than the one requested.
organization_not_found404No workspace matches this organization slug.
domain_not_authorized403The request origin is not an authorized storefront domain for this workspace.
invalid_return_url422successUrl or cancelUrl is not a valid http or https URL.
return_url_not_authorized403The return URL points outside the authorized storefront domains.
unknown_variant422A cart line references a variant that is not on sale in this workspace.
insufficient_stock409Stock ran out between rendering the page and creating the checkout.
shipping_unavailable409No shipping rate covers this cart. The customer can change country or cart, so retrying the same cart will not help.
mixed_currency_cart422One checkout cannot mix currencies. Split the cart.
rate_limited429Too many checkout attempts from this IP. Retry-After says how long to wait.
internal_error500Something failed on our side. Details are recorded by WireGum, not returned.
checkout_unavailable503The workspace is not ready to sell: no Stripe account, no active plan, no shipping rate. A merchant has to change a setting.

A 500 never carries the underlying exception. Database and Stripe failures are recorded on our side and answer with internal_error and one fixed sentence, so a public storefront cannot become a window into what runs behind it. checkout_unavailable is masked the same way: the workspace owner sees which setting is missing inside WireGum, while the shop answers its visitors with one sentence that reveals nothing about the account behind it.

Stability and versioning

The current storefront contract is v1, and it is in the URL: every public endpoint lives under /api/storefront/v1/. Each response also carries a WireGum-Storefront-Version header saying which contract answered. The public widget exposes window.WireGum.version as 1.0.0.

The versioned path is the only public address, and the workspace is named with the organization parameter on both endpoints. There is one spelling for each, so an integration written against this page cannot drift onto a second one.

Stable surface: data attributes, window.WireGum methods, product payload fields used by the widget, checkout request fields, and the error code values with their statuses. Error sentences are not stable: read the code.Additive changes ship on the same contract. A breaking change ships as v2 on its own path, and v1 keeps answering: the widget follows whatever WireGum serves, but a server-side integration moves when it chooses to.Use WireGum public product and variant IDs, not editable slugs or internal database IDs, when CMS integrations must remain stable over time.Read a listing to the end with page.hasMore, never by assuming one response holds the whole catalog. The default page size can grow; a client that pages does not care.