Home / Storefront API

Storefront API

Build your own storefront — Next.js, Astro, mobile, anything — and use Foundry as the inventory and order source of truth. Foundry serves catalog data, validates inventory, and ingests finalized orders. Your storefront owns cart, checkout, customers, and payments.

What you get

  • • Channel-scoped catalog reads (only products you've listed on this channel)
  • • Real-time inventory checks with reservation-aware availability
  • • Server-side total recompute on order create (defends against stale-price attacks)
  • • Idempotent order creation via Idempotency-Key header
  • • Signed outbound webhooks when orders or inventory change (Stripe-style HMAC)
  • • Customer-facing order status lookup without an account system

Quickstart

  1. In the Foundry admin: Sales Channels → New, pick platform Headless, give it a name.
  2. On the channel's settings page, add your storefront's origin(s) to Allowed Origins (e.g., https://shop.example.com).
  3. Settings → API Keys → Create. Pick Storefront, pick the channel, save the fims_sf_* key somewhere safe.
  4. Drop the key in your storefront's .env and hit the API.
curl https://api.foundryims.com/api/v1/storefront/products \
  -H "Authorization: Bearer fims_sf_xxx"

Authentication

Every request needs Authorization: Bearer fims_sf_xxx. The key authenticates against a single Headless sales channel — Foundry rejects admin (fims_*) keys on the storefront surface.

CORS

Browser requests are accepted only from origins in the channel's Allowed Origins list. If the list is empty Foundry serves a wildcard (with a server log warning) — don't ship production storefronts that way. Add specific origins from the channel settings page.

Catalog endpoints

GET /storefront/products

List products listed on this channel. Pagination + filtering.

Query params: category, brand, q, inStockOnly, priceMin, priceMax, sort (name|-name|newest|price|-price), page (default 1), limit (default 50, max 200)

GET /storefront/products/:id

Full detail: variants, options, images, attributes.

GET /storefront/categories

Hierarchical tree (children nested under parents).

GET /storefront/categories/:id/products

Products in a category. Same response shape as /products.

GET /storefront/brands

Flat list.

GET /storefront/search?q=

Text search across product name + description.

Caching: all read endpoints return ETag and Cache-Control: public, max-age=60, stale-while-revalidate=300. Send If-None-Match on follow-up requests to get a 304. Put a CDN in front if you can.

Inventory check

POST /storefront/inventory/check

Real-time availability for a batch of variants — useful right before checkout. Reservation-aware (subtracts pending order reservations).

{
  "items": [
    { "variantId": "uuid-1", "qty": 2 },
    { "variantId": "uuid-2", "qty": 1 }
  ]
}

Response shape:

{
  "items": [
    { "variantId": "uuid-1", "available": true,  "onHand": 14 },
    { "variantId": "uuid-2", "available": false, "onHand": 0 }
  ]
}

Orders

POST /storefront/orders

Submit a finalized order. Foundry recomputes totals from current ChannelSku.price values and rejects with a 422 priceMismatch payload if the client total drifts more than $0.01.

{
  "customer":        { "email": "[email protected]", "name": "Jane Buyer", "phone": "+1..." },
  "shippingAddress": { "line1": "...", "city": "...", "state": "...", "postal": "...", "country": "US" },
  "billingAddress":  { "..." },
  "lineItems": [
    { "variantId": "uuid-1", "quantity": 2, "unitPrice": 19.99 }
  ],
  "shipping": { "method": "USPS Priority", "cost": 8.50 },
  "tax":      { "amount": 3.20 },
  "discount": { "code": "WELCOME10", "amount": 4.00 },
  "payment":  { "provider": "stripe", "chargeId": "ch_xxx", "amount": 47.68 },
  "totals":   { "subtotal": 39.98, "shipping": 8.50, "tax": 3.20, "discount": 4.00, "total": 47.68 }
}

Response on success:

{
  "orderId":     "uuid",
  "orderNumber": "SF-A1B2C3D4",
  "status":      "PROCESSING",
  "lookupToken": "abc123...xyz"
}

Idempotency

Send Idempotency-Key: <unique-per-attempt>. Replaying the same key with the same body returns the original response (no double order). Replaying with a different body returns 409. Keys are retained for 24h.

GET /storefront/orders/:lookupToken

Customer-facing order status. The lookupToken returned at order creation IS the auth — no API key required. Returns sanitized order detail (status, line items, totals, shipping address, tracking). Email is partially redacted. Give this URL to your customer in the order-confirmation email.

POST /storefront/orders/:id/regenerate-lookup-token

Authed. Mints a new lookupToken and invalidates the old one. Useful when a customer says they lost the link.

Webhooks

Skip polling. Configure a Webhook URL on the channel's settings page; Foundry POSTs signed events to it.

Events

Payload shapes for price.changed, product.updated, and product.delisted are reserved but not yet emitted upstream.

Request shape

POST <your webhook URL>
Headers:
  Content-Type:         application/json
  X-Foundry-Event:      order.status_changed
  X-Foundry-Timestamp:  1747526400
  X-Foundry-Delivery-Id: dlv_abc123
  X-Foundry-Signature:  t=1747526400,v1=<hex hmac sha256>
Body:
  { "event": "order.status_changed", "deliveredAt": "...", "data": { ... } }

Verifying signatures

Compute HMAC-SHA256 over {timestamp}.{rawBody} with your webhook secret, compare to the v1 value in the signature header. Stripe-style.

import crypto from "crypto";

function verify(req, secret) {
  const sigHeader = req.headers["x-foundry-signature"];
  const ts        = req.headers["x-foundry-timestamp"];
  const expected  = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${req.rawBody}`)
    .digest("hex");
  const received  = sigHeader.match(/v1=([0-9a-f]+)/)?.[1];
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex"),
  );
}

Retries

Non-2xx (and 429/5xx) responses are retried with exponential backoff: 5s → 30s → 5min → 1h, then terminal. 4xx (except 429) is terminal immediately — your endpoint rejected the shape, no point retrying. Always dedup on X-Foundry-Delivery-Id; the same delivery may fire twice across instance restarts.

Rotating the secret

From the channel settings page (or POST /channels/:id/rotate-webhook-secret). After rotation the previous secret is accepted for 24h so you have a deploy window to roll receivers.

Rate limits

429 responses include Retry-After in seconds.

Errors

Need a working example?

We're shipping a minimal Next.js starter at github.com/Epic-Design-Labs/foundry-storefront-starter that demonstrates list → check → checkout end-to-end. Book a demo and we'll walk you through it.