Skip to content

Latest commit

 

History

History
931 lines (724 loc) · 30.1 KB

File metadata and controls

931 lines (724 loc) · 30.1 KB

BuilderBlack — Storefront API

Reference for frontend developers building shopper-facing storefronts (Next.js, Remix, Astro, native mobile apps, kiosks) against BuilderBlack stores.

Audience: you're writing code that buyers see — product pages, cart, checkout. You hold the relationship with the merchant's tenant. If you're building partner apps (analytics, ERP sync, marketing automations) instead, see PUBLIC-API.md. If you're a merchant operating a store, see DASHBOARD-GUIDE.md.

  • Base URL: https://api.builderblack.com
  • Version: v1 (all paths begin /api/v1/)
  • Auth model: anonymous endpoints + per-shopper customer JWT + an optional read-only Storefront API token for server-side renders
  • Source of truth: this repo (apps/api/src/)

Table of contents

  1. Three deployment modes
  2. Authentication
  3. CORS + tenant resolution
  4. Storefront SDK (recommended path)
  5. Cart session model
  6. Customer auth (registered shoppers)
  7. Resource reference
  8. Checkout flow
  9. Webhooks for storefronts
  10. Performance notes
  11. Errors
  12. Migrating from Shopify Storefront API
  13. Hard constraints

1. Three deployment modes

A storefront can talk to our API in three configurations. Pick the one that fits how you want to host the buyer-facing experience.

┌──────────────────────────────────────────────────────────────────────┐
│  Mode A: Platform-rendered                                           │
│    <slug>.builderblack.com  →  our Next.js (apps/web)                │
│    No code from you. Merchant picks a theme in /customize.           │
│    Use this when the merchant doesn't have engineers.                │
└──────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────┐
│  Mode B: Self-hosted                                                 │
│    yourbrand.com  →  YOUR server (Vercel / your VPS / wherever)      │
│    YOUR Next.js / Remix / native app calls api.builderblack.com.     │
│    Tenant has deployment_mode='self_hosted'; we don't generate an    │
│    nginx vhost or cert for the domain — you handle SSL on your end.  │
│    Use this when you want full design freedom + your own perf.       │
└──────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────┐
│  Mode C: Native / kiosk / voice                                      │
│    Mobile app, in-store screen, Alexa skill, Telegram bot, etc.      │
│    No domain involved. Calls api.builderblack.com directly with the  │
│    storefront token + customer JWT for logged-in shoppers.           │
└──────────────────────────────────────────────────────────────────────┘

This document covers all three. Differences are flagged inline.


2. Authentication

There are three auth surfaces. Use them in combination.

2.1 Anonymous (no auth header)

Most public endpoints work with no auth — just x-tenant-slug so we know which store you mean:

GET /api/v1/products?limit=24
Host:           api.builderblack.com
x-tenant-slug:  kartse

For Mode A (platform-rendered) and Mode B (self-hosted via custom domain registered in tenant_domains), the Host header alone resolves the tenant. x-tenant-slug is optional but recommended for explicitness.

2.2 Storefront API token (recommended for server-side fetches)

A read-only token bound to one tenant. Issue from the dashboard under Settings → API Tokens. Looks like bb_sft_<48 hex>.

GET /api/v1/products?limit=24
Authorization:  Bearer bb_sft_4f2e8a3b...

Use this when:

  • You're rendering server-side (Next.js getServerSideProps, ISR revalidate hooks, edge functions). The token lets you bypass per-IP rate limits and gets a higher cache budget.
  • You're doing static generation that pre-fetches product data at build time.
  • You're a kiosk / native app with no user logged in yet — the token identifies the tenant without needing a custom domain.

The token grants:

  • ✅ Read all anonymous endpoints (products, collections, navigation, pages, FX, theme settings)
  • ✅ Create cart sessions + add/remove items (rate-limited)
  • ❌ Mutations on products / orders / customers / settings (use the Partner API for that — different auth flow)
  • ❌ Read other tenants (token is tenant-scoped)

2.3 Customer JWT (per shopper)

When a buyer logs in, you call our auth endpoints and get back a JWT. Send it as Authorization: Bearer <jwt> on shopper-specific endpoints (/api/v1/me/*).

See Section 6 for the full flow.

2.4 Combining auth sources

A typical authenticated storefront request stack:

Authorization:    Bearer <customer_jwt>     # shopper identity
x-tenant-slug:    kartse                    # which store
X-Cart-Session:   <uuid>                    # guest cart fallback if not logged in
Idempotency-Key:  <uuid>                    # for write requests

Most endpoints work with any one of token/JWT/session header. The combination above is the safest defaults bundle.


3. CORS + tenant resolution

How we resolve tenants

The server checks (in order):

  1. x-tenant-id header
  2. x-tenant-slug header
  3. ?tenant=<slug> query param
  4. :tenantSlug URL path param (some endpoints)
  5. Custom domain matchHost header looked up in tenant_domains (must be verified=true)
  6. Subdomain match<slug>.builderblack.com from Host

Source: apps/api/src/middleware/tenantResolver.ts.

CORS

Our CORS plugin allows:

  • app.builderblack.com, admin.builderblack.com — credentialed
  • Tenant subdomains *.builderblack.com — non-credentialed
  • Verified custom domains in tenant_domains — non-credentialed

For Mode B (self-hosted), your domain MUST be verified by the merchant before CORS will allow it. The merchant adds it under Domain & SSL in their dashboard. Set deployment_mode='self_hosted' so we know not to provision an nginx vhost on our end.

CORS-allowed request headers:

Content-Type, Authorization, x-tenant-id, x-tenant-slug,
x-csrf-token, x-cart-session, idempotency-key, x-request-id

If you need to send a header not on this list, the merchant has to add it via their CORS allowlist (or you ask us to extend the default).

CORS-exposed response headers:

x-request-id, x-api-version, deprecation, sunset, link

Source: apps/api/src/plugins/cors.ts.


4. Storefront SDK (recommended path)

Don't write raw fetch() calls — use our typed SDK:

npm install @builderblack/storefront-sdk
import { createStorefrontClient } from '@builderblack/storefront-sdk';

// In a Next.js project: lib/storefront-client.ts
export const sf = createStorefrontClient({
  apiUrl:           process.env.NEXT_PUBLIC_BB_API_URL!,
  tenantSlug:       process.env.NEXT_PUBLIC_BB_TENANT_SLUG!,
  storefrontToken:  process.env.BB_STOREFRONT_TOKEN, // optional, server-only
  cartSessionId:    typeof window !== 'undefined'
                       ? localStorage.getItem('bb_cart_session') ?? undefined
                       : undefined,
});

// In a page / API route:
const { data: products } = await sf.products.list({ limit: 24 });
const { data: cart }     = await sf.cart.addItem({ variantId, quantity: 1 });

The SDK handles:

  • Tenant header injection
  • Cart session persistence (browser localStorage)
  • Customer JWT refresh
  • Idempotency key generation for writes
  • Type-safe responses (zod schemas exported from @builderblack/shared)
  • Universal (browser + Node + edge runtimes)

Source: packages/storefront-sdk/. Reference implementation: github.com/builderblack/storefront-starter.


5. Cart session model

Buyers shop with one of two cart identities:

Guest cart (no login)

Browser generates a UUID once and stores it in localStorage. Sends it as X-Cart-Session: <uuid> on every cart call.

// First page load — generate + persist
let sessionId = localStorage.getItem('bb_cart_session');
if (!sessionId) {
  sessionId = crypto.randomUUID();
  localStorage.setItem('bb_cart_session', sessionId);
}

// Every cart call carries the header
fetch('/api/v1/cart', { headers: { 'X-Cart-Session': sessionId } });

The cart row persists in our carts table for 14 days from last activity, so a returning visitor sees their cart.

Customer cart (logged in)

Once the buyer logs in, we identify the cart by customer_id from the JWT. The X-Cart-Session header is ignored if a customer JWT is also present.

Login-merge

When a guest logs in, we merge their guest cart into the customer cart automatically (mergeGuestIntoCustomer() in apps/api/src/modules/auth/auth.service.ts):

  • Same variant in both → quantities sum
  • Different variants → all kept
  • Customer cart wins on prices (in case prices changed since the guest added the item)

Cart endpoints

Method Path Description
GET /api/v1/cart Fetch the cart (creates empty if missing)
PUT /api/v1/cart Replace the entire items array (used after optimistic local mutations)
POST /api/v1/cart/items Add one line item ({ variantId, quantity }); applies stock check
PATCH /api/v1/cart/items/:variantId Update quantity; quantity <= 0 removes
DELETE /api/v1/cart/items/:variantId Remove one line item
DELETE /api/v1/cart Clear all items

Source: apps/api/src/modules/cart/cart.routes.ts.

Cart object shape

{
  "id":         "uuid",
  "items": [
    {
      "variant_id": "uuid",
      "product_id": "uuid",
      "title":      "Widget Pro - Red / Large",
      "image_url":  "https://api.builderblack.com/files/store-acme/media/...webp",
      "sku":        "WID-PRO-RED-L",
      "price":      "1499.00",
      "quantity":   2,
      "subtotal":   "2998.00"
    }
  ],
  "subtotal":      "2998.00",
  "currency":      "INR",
  "updated_at":    "2026-05-08T10:30:00.000Z"
}

Note: cart does not include shipping, tax, or final total. Those are computed at checkout when the buyer enters an address — see Section 8.


6. Customer auth (registered shoppers)

Registering

POST /api/v1/auth/customer/register
Content-Type:  application/json
x-tenant-slug: kartse

{
  "email":      "buyer@example.com",
  "password":   "8+ chars",
  "first_name": "Asha",
  "last_name":  "Sharma",
  "phone":      "+919876543210"   // optional
}

201 Created
{
  "success": true,
  "data": {
    "customer":      { "id": "uuid", "email": "buyer@example.com", ... },
    "access_token":  "eyJ...",      // 24h TTL
    "refresh_token": "...",         // 30d TTL
    "email_verified": false
  }
}

We send a verification email automatically. Until verified, /api/v1/me/* endpoints return 403 with code: EMAIL_NOT_VERIFIED.

Logging in

POST /api/v1/auth/customer/login
Content-Type:  application/json
x-tenant-slug: kartse

{ "email": "buyer@example.com", "password": "..." }

200 OK
{
  "success": true,
  "data": {
    "customer":      { ... },
    "access_token":  "eyJ...",
    "refresh_token": "...",
    "email_verified": true
  }
}

Refreshing the access token

Access tokens expire in 24h. Use the refresh token (30d):

POST /api/v1/auth/customer/refresh
Content-Type: application/json

{ "refresh_token": "..." }

The SDK does this automatically when it sees a 401 with code: TOKEN_EXPIRED.

Forgot password

POST /api/v1/auth/customer/forgot-password
{ "email": "buyer@example.com" }

Always returns 200 (don't leak whether the email exists). We email the reset link.

POST /api/v1/auth/customer/reset-password
{ "token": "...", "new_password": "..." }

Customer endpoints (/api/v1/me/*)

Once authenticated, shoppers access their own data:

Method Path Description
GET /api/v1/me Profile
PATCH /api/v1/me Update profile (name, phone)
GET /api/v1/me/addresses Saved addresses
POST /api/v1/me/addresses Add address
PUT /api/v1/me/addresses/:id Update
DELETE /api/v1/me/addresses/:id Remove
GET /api/v1/me/orders Order history
GET /api/v1/me/orders/:id Single order detail
POST /api/v1/me/orders/:id/cancel Cancel an unshipped order
GET /api/v1/me/wishlist Wishlist items
POST /api/v1/me/wishlist Add to wishlist
DELETE /api/v1/me/wishlist/:productId Remove
GET /api/v1/me/gift-cards Gift card balances

Source: apps/api/src/modules/customers/me-account.routes.ts.


7. Resource reference

Everything below is anonymous-callable (no JWT required), with optional Bearer bb_sft_<token> for higher rate limits + caching.

7.1 Products

Method Path Description
GET /api/v1/products List. Query: limit, cursor, q, status, tag, vendor, collection_id, sort
GET /api/v1/products/slug/:slug Single product by storefront slug (most common — your URLs use slug)
GET /api/v1/products/:id Single product by UUID

Sample list response:

{
  "success": true,
  "data": [
    {
      "id":          "uuid",
      "title":       "Widget Pro",
      "slug":        "widget-pro",
      "description": "<p>HTML-sanitised description</p>",
      "vendor":      "Acme",
      "currency":    "INR",
      "min_price":   "1499.00",
      "max_price":   "2999.00",
      "thumbnail":   "https://api.builderblack.com/files/store-acme/media/...webp",
      "tags":        ["new", "featured"],
      "in_stock":    true
    }
  ],
  "next_cursor": "eyJpZCI6IjEyMyJ9",
  "total":       42
}

For the full product (with variants + images + features), call /api/v1/products/slug/widget-pro — different shape, more data.

7.2 Collections

Method Path Description
GET /api/v1/collections List collections
GET /api/v1/collections/slug/:slug Single collection (with metadata)
GET /api/v1/collections/:id/products Products in collection. Query: limit, cursor, sort

7.3 Navigation menus

Method Path Description
GET /api/v1/navigation/:menuKey Menu tree. menuKey typically header, footer, mobile, legal

Used for header / footer / drawer rendering.

7.4 Pages

Method Path Description
GET /api/v1/pages/:slug Single CMS page (HTML body, sanitised)

For /pages/about, /pages/privacy, /pages/refund-policy, etc.

7.5 Theme settings

Method Path Description
GET /api/v1/theme/live Active theme payload (settings + section config) for the tenant

Use this in Mode B / C to mirror the dashboard /customize settings (brand colors, fonts, hero copy) without redeploying your storefront.

7.6 Stock notifications

Method Path Description
POST /api/v1/stock-notifications Subscribe to back-in-stock alerts

Body: { email, product_id, variant_id }.

7.7 Gift cards

Method Path Description
POST /api/v1/gift-cards/check Check balance by code (used at checkout). No auth needed.

7.8 FX (foreign exchange rates)

Method Path Description
GET /api/v1/public/fx/rates Daily rates from frankfurter.app, base USD

Use to show approximate prices in a buyer's local currency on the storefront. Authoritative pricing still happens at checkout in the store's settlement currency.

7.9 Tenant resolution helpers

Method Path Description
GET /api/v1/public/tenants/resolve?host=<hostname> Resolve a hostname to a tenant slug (useful for SSR routing)
GET /api/v1/public/tenants/:slug/primary-host Look up a tenant's canonical hostname

8. Checkout flow

This is the core flow your code will exercise heavily. Follow it step-by-step.

Step 1 — Get cart shipping rates

POST /api/v1/checkout/shipping-rates
Content-Type:  application/json
x-tenant-slug: kartse
X-Cart-Session: <uuid>           # or Bearer <customer_jwt>

{
  "address": {
    "country":     "IN",
    "state":       "Karnataka",
    "postal_code": "560001",
    "city":        "Bengaluru"
  }
}

200 OK
{
  "success": true,
  "data": {
    "methods": [
      { "id": "standard", "label": "Standard (3-5 days)", "rate": "60.00" },
      { "id": "express",  "label": "Express (1-2 days)",  "rate": "180.00" }
    ]
  }
}

Step 2 — Get tax quote

POST /api/v1/checkout/tax-quote
Content-Type:  application/json

{
  "address":  { "country": "IN", "state": "Karnataka", "postal_code": "560001" },
  "subtotal": "1499.00"
}

200 OK
{
  "success": true,
  "data": {
    "tax_total":  "269.82",
    "rate":       0.18,
    "breakdown": [
      { "label": "CGST 9%", "amount": "134.91" },
      { "label": "SGST 9%", "amount": "134.91" }
    ]
  }
}

Step 3 — Apply discount code (optional)

POST /api/v1/discounts/validate
Content-Type:  application/json

{
  "code":     "WEEKEND10",
  "subtotal": "1499.00",
  "items":    [{ "variant_id": "...", "quantity": 1 }]
}

200 OK
{
  "success": true,
  "data": {
    "type":     "percentage",
    "value":    10,
    "discount": "149.90",
    "label":    "WEEKEND10 — 10% off"
  }
}

Step 4 — Place the order

POST /api/v1/orders
Content-Type:    application/json
Idempotency-Key: <uuid>            # IMPORTANT — see below
x-tenant-slug:   kartse
X-Cart-Session:  <uuid>            # or Bearer <customer_jwt>

{
  "customer": {
    "email":      "buyer@example.com",
    "first_name": "Asha",
    "last_name":  "Sharma",
    "phone":      "+919876543210"
  },
  "shipping_address": { "country": "IN", "state": "...", "city": "...", "line1": "...", "postal_code": "560001" },
  "billing_address":  { /* same as shipping or different */ },
  "shipping_method":  "standard",
  "payment_method":   "razorpay",   // razorpay | stripe | cod | manual | gift_card
  "discount_code":    "WEEKEND10",  // optional
  "gift_card_code":   "...",        // optional
  "notes":            "Leave at door"
}

201 Created
{
  "success": true,
  "data": {
    "order_id":        "uuid",
    "order_number":    "1042",
    "status":          "pending",
    "currency":        "INR",
    "subtotal":        "1499.00",
    "discount_total":  "149.90",
    "shipping_total":  "60.00",
    "tax_total":       "242.83",
    "grand_total":     "1651.93",
    "payment_required": true,
    "payment_intent": {
      "provider": "razorpay",
      "razorpay_order_id": "order_LkX7v...",
      "amount":            165193,           // paise
      "currency":          "INR",
      "key":               "rzp_live_..."
    }
  }
}

Idempotency-Key is strongly recommended — without it, a network retry can place a duplicate order. Use a UUID per checkout button click; same key within 24h returns the cached response.

Step 5 — Complete payment

For Razorpay — use their checkout SDK with payment_intent data:

<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<script>
  const rzp = new Razorpay({
    key:      paymentIntent.key,
    amount:   paymentIntent.amount,
    currency: paymentIntent.currency,
    order_id: paymentIntent.razorpay_order_id,
    handler: async (response) => {
      // On payment success, our server hears the webhook independently
      // and confirms the order. The handler can just navigate to the
      // confirmation page — order will be updated by the time it loads.
      window.location.href = `/order-confirmed/${orderId}`;
    },
  });
  rzp.open();
</script>

For Stripe — use Stripe Elements with payment_intent.client_secret.

For COD — order is pending until merchant confirms. Show the buyer the confirmation page directly; they don't pay now.

Step 6 — Confirmation page

GET /api/v1/orders/confirmation/:orderId

200 OK   (works without auth for 7 days post-checkout)
{ "success": true, "data": { /* order with items, addresses, totals */ } }

Use this on /order-confirmed/:id so the buyer sees their order without needing to log in.


9. Webhooks for storefronts

Most webhook events are for partner apps, not storefronts. But two patterns matter for buyers:

Real-time order updates (server-sent events)

Coming in a future release — for now, poll /api/v1/me/orders/:id every 30s on the order-confirmed page until status flips from pending to confirmed.

Stock notifications

Buyer subscribes to back-in-stock via POST /api/v1/stock-notifications. We email them when the variant restocks. No webhook to your storefront needed.

Custom webhook subscriptions

If you want order-created / order-updated webhooks delivered to your own server (e.g. to update an external CRM after a checkout), you need a partner app — not a storefront. See PUBLIC-API.md section 6.


10. Performance notes

HTTP caching

All anonymous GET endpoints set:

Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600

This means:

  • Browser caches for 60s
  • CDN edges cache for 5 minutes
  • For 10 minutes after expiry, the CDN can serve stale + revalidate in the background

In Mode B/C with the Storefront API token, you get more aggressive caching: s-maxage=3600 (1h CDN). Set up a reverse proxy (Vercel, Cloudflare Workers, your own Varnish) and your storefront serves products at edge speed.

Bust the cache by:

  • Bumping the product version (we set ETag on every response)
  • Subscribing to product.updated webhooks in a partner app and purging your CDN

Keep-alive

The SDK uses HTTP keep-alive by default. Don't open + close connections per request — reuse the client.

Image variants

Every uploaded image has 4 responsive variants:

  • thumb (200px) — listing thumbnails
  • sm (480px) — mobile cards
  • md (960px) — desktop cards / mobile PDP
  • lg (1600px) — desktop PDP / hero

URLs come back as { url, variants: [{ label, width, url }] }. Use the smallest one that works for the slot to save bandwidth.

<img
  src={product.thumbnail}
  srcset={product.images[0].variants
    .map(v => `${v.url} ${v.width}w`)
    .join(', ')}
  sizes="(max-width: 768px) 100vw, 50vw"
  alt={product.title}
/>

Request batching

We don't have GraphQL yet, so a typical product page does ~4 round trips:

  1. /api/v1/products/slug/:slug — the product
  2. /api/v1/collections/:id/products?limit=4 — related products
  3. /api/v1/cart — current cart count (for header)
  4. /api/v1/theme/live — theme settings (cache aggressively, only changes when merchant edits)

In Next.js, fire these in Promise.all from getServerSideProps so they happen in parallel.


11. Errors

Same envelope as the partner API:

{
  "success": false,
  "error":   "Human-readable message",
  "code":    "MACHINE_CODE",
  "details": { /* optional */ }
}

Common codes for storefronts:

HTTP code Meaning
400 INVALID_INPUT Validation failed
401 UNAUTHORIZED JWT missing / expired (refresh)
403 EMAIL_NOT_VERIFIED Customer hasn't verified email yet
403 TENANT_INACTIVE Store is suspended (don't show shop UI)
404 NOT_FOUND Product / order / page doesn't exist
404 TENANT_NOT_FOUND Wrong tenant slug or unverified domain
409 OUT_OF_STOCK Cart-add exceeds available inventory
409 IDEMPOTENCY_KEY_REUSED_DIFFERENT_BODY Same key, different payload — reset key per attempt
422 DISCOUNT_NOT_APPLICABLE Code doesn't match cart contents
422 PAYMENT_FAILED Payment provider declined
429 RATE_LIMITED Slow down; respect Retry-After

12. Migrating from Shopify Storefront API

If you already have a Hydrogen / Storefront-Kit codebase, the porting effort is mostly mechanical.

Shopify BuilderBlack Notes
Storefront Access Token (shpat_...) Storefront API Token (bb_sft_...) Same idea — header Authorization: Bearer ...
GraphQL Storefront API REST /api/v1/* We're REST today; GraphQL is a roadmap item
/api/2024-04/... versioned path /api/v1/... Different versioning scheme; same idea
productByHandle(handle: $h) GET /api/v1/products/slug/:slug "handle" → "slug"
cartCreate mutation Implicit — POST /api/v1/cart/items creates the cart on first call No explicit cart-create needed
cartLinesAdd POST /api/v1/cart/items One item at a time today; bulk-add roadmap
checkoutCreate + checkoutCompleteWithCreditCard POST /api/v1/orders Single endpoint that returns payment_intent
Webhook orders/create Same — but subscribed via partner app, not storefront See PUBLIC-API.md §6
customerAccessToken Customer JWT Same idea, different format
Hydrogen <Money /> component Use the formatPrice helper from the SDK or roll your own with Intl.NumberFormat We export currency settings via /api/v1/theme/live

Differences worth flagging:

  • We have multi-currency on every store without a paid tier. Read currency + currency_symbol + currency_position from /api/v1/theme/live instead of guessing from locale.
  • HSN code on every product (Indian tax). Display on B2B invoices, ignore for B2C.
  • COD as a payment method is first-class. Show it conditionally based on the buyer's pincode (configured in the store's payment methods).
  • No customer addresses on initial order — buyers can place orders as guests; the address goes on the order, not the customer record. The customer record gets the address only after they register or save it.

13. Hard constraints

These keep your storefront within the platform's contract. Violating them gets your domain rate-limited or removed from CORS.

  1. Never expose the Storefront API token to browser JS. It's a server-only credential. Browser code uses anonymous endpoints + customer JWT. Token in browser DevTools = hand it back, rotate it, investigate.

  2. Don't poll /api/v1/products faster than once per minute per tenant per IP. That's 60 product-list requests per minute. If you need fresh data, subscribe to product.updated webhooks via a partner app.

  3. Don't try to confirm your own orders. The order's financial_status flips from pending to paid only after our server hears the payment webhook from Razorpay/Stripe. Don't write client code that "trusts" the buyer's payment-success callback — it can be spoofed.

  4. Don't bypass Idempotency-Key on /api/v1/orders. Every missed retry without the key risks duplicate charges. The SDK adds the key automatically; if you go raw fetch(), you do it.

  5. Don't proxy our API through your own server then expose those responses without filtering. If you SSR a page that includes a product price, never include the merchant's cost_price (your buyers would see margin). The product list endpoint doesn't return cost_price, but be careful about what you forward.

  6. Don't render unsanitised HTML from the product description / page body. We sanitise on save (P17), but you should still render through dangerouslySetInnerHTML only with a known-safe sanitiser on your end if you fetch via a non-trusted path.

  7. Don't store customer JWTs in localStorage if you can avoid it. Use httpOnly cookies set by your backend for the auth handoff. The SDK will help with this in v0.2.

  8. Don't keep a customer JWT past expiry without refreshing it. Stale tokens get the UNAUTHORIZED code; if your code keeps retrying with a stale token, you'll trip the auth-rate-limit (10/min).

  9. Don't change tenant_slug mid-session. A storefront serves ONE tenant. Mixing tenants in the same client instance is a bug and tripping our anti-pivot rate limit will throttle you hard.

  10. Don't fetch /api/v1/admin/* from a storefront. Those are dashboard endpoints — they require platform-user JWT, not customer JWT. Storefront callers stay on /api/v1/* (anonymous + /me) and the resources listed in Section 7.


Appendix: contact + support

  • Storefront SDK + starter:
  • Partner-app reference (different audience): PUBLIC-API.md
  • Bug reports / feature requests: partners@builderblack.com
  • Security disclosures: security@builderblack.com

When reporting an issue, include the x-request-id header from a failed response — we log every request with this id end-to-end.