Storefront API

The Storefront API powers embedded storefronts and the custom product designer on SimplerDevelopment-hosted tenant websites. It handles the full transactional surface: browsing products, managing a shopping cart, running checkout via Stripe, looking up orders, and the complete custom-product-designer lifecycle (create, edit, AI-generated artwork, share, add to cart).

This API is distinct from the read-only Commerce API, which is keyed with a platform API key and is designed for third-party integrations. The Storefront API has no platform API key — it is called directly from the storefront JavaScript running on the customer's browser.

Base URL: https://{your-storefront-domain}/api/storefront/{siteId}/...

The {siteId} is the numeric ID of the tenant website (a clientWebsites.id). Every endpoint requires a valid, enabled store for that site or returns 404 Store not found.


Authentication model#

The Storefront API has three auth tiers. Read this section before writing any client code.

Public endpoints — no credential required. These are the product catalog, categories, cart operations, checkout, guest order lookup, discount/gift-certificate validation, and shipping rate queries. Anyone who knows the siteId can call them.

Customer session (Bearer token) — required for account, order history, wishlist, and support endpoints. After a customer logs in or registers via POST /auth, the response includes a token. Pass it on every protected call:

Authorization: Bearer <token>

The token is opaque to the client; the server validates it against a server-side session store. There is no cookie-based auth for customers — the Bearer header is the only mechanism.

Designer session (cookie) — used by the custom product designer for anonymous visitors. When POST /designs creates a design for a visitor who has no customer token, the server mints an anonymous session ID and sets it in a first-party cookie (sd_design_session). Subsequent designer calls read that cookie automatically. Logged-in customers use their Bearer token instead of the cookie; the server accepts both.


Cart state model#

The cart is server-side and keyed by a client-generated sessionId string (a UUID you create in localStorage and pass on every cart call). There are no cart cookies. A cart is created automatically on the first POST /cart call for a new sessionId, and expires 7 days after last activity. Carts that go dormant can enter an abandoned state and be reactivated via GET /cart/recover.


Products#

GET/products#

Returns a paginated list of active products.

  • Auth: Public

Query params

NameTypeDefaultDescription
categorystringFilter by category slug
searchstringKeyword search against product name and short description
sortstringnewestnewest, price_asc, price_desc, or featured
pageinteger1Page number (1-based)
limitinteger24Results per page (max 100)

Response

{
  "success": true,
  "data": [
    {
      "id": 42,
      "name": "Classic Crew Tee",
      "slug": "classic-crew-tee",
      "shortDescription": "A comfortable everyday crew-neck tee.",
      "price": 2500,
      "compareAtPrice": 3200,
      "featured": true,
      "categoryId": 7,
      "createdAt": "2025-03-15T10:22:00.000Z",
      "image": "https://cdn.example.com/products/classic-crew-tee.jpg",
      "categoryName": "T-Shirts"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 24,
    "total": 58,
    "totalPages": 3
  }
}

price and compareAtPrice are in cents. image is the first image by display order, or null. categoryId and categoryName are null for uncategorized products.


GET/products/{slug}#

Returns a full product record including images, options with values, active variants, bulk pricing rules, and category. The {slug} segment accepts a URL slug (storefront) or a numeric product ID (product designer editor).

  • Auth: Public

Response — same shape as Commerce API GET /products/{slug} including images, options, variants, bulkPricing, and category.

When called with a numeric product ID instead of a slug, the response includes styles (product designer styles with nested sides) instead of the standard commerce fields.


GET/products/{slug}/styles#

Returns designer styles and their printable sides for a product. Used by the custom product designer to build the canvas.

  • Auth: Public

Response

{
  "success": true,
  "data": [
    {
      "id": 5,
      "productId": 42,
      "name": "White",
      "active": true,
      "order": 0,
      "sides": [
        {
          "id": 11,
          "styleId": 5,
          "name": "Front",
          "slug": "front",
          "mockupImage": "https://cdn.example.com/mockups/white-front.png",
          "printAreaX": 120,
          "printAreaY": 80,
          "printAreaWidth": 280,
          "printAreaHeight": 320,
          "order": 0
        }
      ]
    }
  ]
}

Categories#

GET/categories#

Returns all active product categories ordered by order then name, with a live count of active products per category.

  • Auth: Public

Response

{
  "success": true,
  "data": [
    {
      "id": 7,
      "name": "T-Shirts",
      "slug": "t-shirts",
      "description": "All of our tee styles.",
      "image": "https://cdn.example.com/categories/t-shirts.jpg",
      "parentId": null,
      "order": 1,
      "productCount": 14
    }
  ]
}

parentId is null for top-level categories; set to another category's id for subcategories.


Cart#

GET/cart#

Fetches the active cart for a session, with enriched line items (product name, slug, variant name, first product image, and any attached design thumbnail).

  • Auth: Public

Query params

NameTypeRequiredDescription
sessionIdstringYesClient-generated cart session identifier

Response

{
  "success": true,
  "data": {
    "cartId": 301,
    "items": [
      {
        "id": 1501,
        "productId": 42,
        "variantId": 201,
        "designId": null,
        "quantity": 2,
        "unitPrice": 2500,
        "lineTotal": 5000,
        "productName": "Classic Crew Tee",
        "productSlug": "classic-crew-tee",
        "variantName": "Classic Crew Tee – S",
        "image": "https://cdn.example.com/products/classic-crew-tee.jpg",
        "design": null
      }
    ],
    "subtotal": 5000,
    "itemCount": 2
  }
}

An empty or non-existent cart returns { success: true, data: { items: [], subtotal: 0 } } — never a 404.


POST/cart#

Adds a product (optionally a specific variant, optionally with a saved custom design) to the cart. Creates the cart if one doesn't exist yet for the sessionId. Existing same-product + same-variant + same-design lines are merged by incrementing quantity; designed items are always added as new lines.

  • Auth: Public

Request body

FieldTypeRequiredDescription
sessionIdstringYesCart session identifier
productIdintegerYesProduct to add
variantIdintegerNoSpecific variant; omit for base product
quantityintegerNo (default 1)Units to add
designIdstring (UUID)NoSaved custom design to attach to this line

Response — returns the inserted or updated cart item row.


PUT/cart#

Updates the quantity of an existing cart line. Setting quantity to 0 removes the item.

  • Auth: Public

Request body

FieldTypeRequiredDescription
cartItemIdintegerYesID of the cart item to update
quantityintegerYesNew quantity (0 = remove)

DELETE/cart#

Clears all items from the active cart for a session.

  • Auth: Public

Query params

NameTypeRequiredDescription
sessionIdstringYesCart session identifier

Response

{ "success": true, "data": { "cleared": true } }

GET/cart/recover#

Reactivates an abandoned cart via a single-use recovery token (sent by cart-abandonment automations). Clears the token on use and redirects the browser to {storeBaseUrl}/store/cart?recovered=1 with HTTP 302.

  • Auth: Public

Query params

NameTypeRequiredDescription
tokenstringYesSingle-use recovery token from the abandonment email

Errors

StatusMessageCause
400token is requiredMissing token
404Invalid or expired recovery tokenToken not found, already used, or expired

Checkout#

POST/checkout#

Validates the cart, calculates the order total (with optional shipping, discount code, gift certificate, and tax), creates a Stripe PaymentIntent, persists the order, and returns a clientSecret so the browser can confirm payment via Stripe.js without a second round-trip.

  • Auth: Public

Request body

FieldTypeRequiredDescription
sessionIdstringYesCart session identifier
customerEmailstringYesPurchaser email
customerNamestringYesPurchaser display name
customerPhonestringNoPhone number
shippingAddressobjectNoShipping address (free-form JSON)
billingAddressobjectNoBilling address (free-form JSON)
shippingRateIdintegerNoID from GET /shipping; omit for digital or free shipping
discountCodestringNoDiscount code to apply
giftCertificateCodestringNoGift certificate code to redeem
customerNotestringNoNote to attach to the order

Response

{
  "success": true,
  "data": {
    "clientSecret": "pi_3Abc123_secret_xyz",
    "publishableKey": "pk_live_...",
    "orderId": 9001,
    "orderNumber": "ORD-0042",
    "total": 7230,
    "currency": "USD"
  }
}

The Stripe payment flow: (1) call POST /checkout to get clientSecret and publishableKey; (2) initialise Stripe(publishableKey) in the browser; (3) call stripe.confirmPayment({ clientSecret, ... }) to capture funds. The order is created in pending status at step (1); a Stripe webhook (handled server-side) marks it paid after step (3) succeeds.

All monetary amounts are in cents. currency is the store's configured ISO currency code.

Errors

StatusMessageCause
400sessionId, customerEmail, and customerName are requiredMissing required fields
400Cart is emptyThe session has no active cart items
400Invalid discount codeCode not found, expired, exceeded uses, or minimum not met
400Invalid shipping rateshippingRateId not valid for this store
404Cart not foundNo active cart for the sessionId

Orders#

GET/orders/{orderNumber}#

Guest order lookup — returns a full order record without requiring a customer login, but requires the purchaser's email for verification.

  • Auth: Public (email verification required)

Query params

NameTypeRequiredDescription
emailstringYesEmail address used when placing the order

Response

{
  "success": true,
  "data": {
    "orderNumber": "ORD-0042",
    "status": "shipped",
    "paymentStatus": "paid",
    "customerName": "Jane Smith",
    "customerEmail": "jane@example.com",
    "shippingAddress": { "line1": "123 Main St", "city": "Portland", "state": "OR", "postalCode": "97201", "country": "US" },
    "billingAddress": null,
    "subtotal": 5000,
    "shippingTotal": 995,
    "taxTotal": 385,
    "discountTotal": 0,
    "total": 6380,
    "shippingMethod": "USPS Priority Mail",
    "carrier": "USPS",
    "trackingNumber": "9400111899222..."
    "trackingUrl": "https://tools.usps.com/go/TrackConfirmAction?tLabels=...",
    "latestTrackingStatus": "In Transit",
    "latestTrackingEventAt": "2026-06-22T14:30:00.000Z",
    "paidAt": "2026-06-20T09:00:00.000Z",
    "shippedAt": "2026-06-21T12:00:00.000Z",
    "deliveredAt": null,
    "createdAt": "2026-06-20T08:55:00.000Z",
    "items": [ { "productName": "Classic Crew Tee", "variantName": "S", "sku": "CCT-001-S", "unitPrice": 2500, "quantity": 2, "total": 5000 } ],
    "trackingEvents": [
      { "processedAt": "2026-06-22T14:30:00.000Z", "eventType": "tracker.updated", "payload": { ... } }
    ]
  }
}

Discounts#

POST/discount/validate#

Validates a discount code and optionally calculates the discount amount for a given subtotal. Does not apply or consume the code — that happens at checkout.

  • Auth: Public

Request body

FieldTypeRequiredDescription
codestringYesDiscount code to validate
subtotalintegerNoCart subtotal in cents; enables discountAmount in the response

Response

{
  "success": true,
  "data": {
    "code": "SUMMER20",
    "description": "20% off your order",
    "discountType": "percent",
    "amount": 2000,
    "minOrderAmount": null,
    "discountAmount": 1000
  }
}

discountType is percent (amount in basis points, e.g. 2000 = 20%), fixed_amount (amount in cents), or free_shipping. discountAmount is null when no subtotal is provided.


Shipping#

GET/shipping#

Returns available shipping rates for a destination. Combines manual rates configured in the portal with optional live carrier rates from EasyPost and/or Printful print-on-demand rates, depending on the store's configuration.

  • Auth: Public

Query params

NameTypeRequiredDescription
countrystringYesISO 3166-1 alpha-2 country code (e.g. US)
statestringNoState / province code
postalCodestringNoPostal code — required for live EasyPost rates
citystringNoCity name (used for Printful recipient)
parcelJSON stringNoCustom parcel dimensions: {"lengthIn":6,"widthIn":4,"heightIn":2,"weightOz":8}
variantIdsstringNoComma-separated variant IDs for Printful POD rate lookup
productIdsstringNoComma-separated product IDs for Printful POD items without a variant
recipientNamestringNoCustomer name for Printful recipient (default: "Customer")
emailstringNoCustomer email for Printful recipient

Response

{
  "success": true,
  "data": [
    {
      "id": 12,
      "name": "Standard Shipping",
      "rateType": "flat",
      "price": 595,
      "freeAbove": 5000,
      "minDeliveryDays": 3,
      "maxDeliveryDays": 7,
      "zoneName": "Domestic"
    },
    {
      "id": "live:rate_abc123",
      "name": "USPS Priority Mail",
      "rateType": "live",
      "price": 995,
      "freeAbove": null,
      "minDeliveryDays": 2,
      "maxDeliveryDays": 2,
      "zoneName": "Live carrier rate",
      "provider": "easypost",
      "carrier": "USPS",
      "service": "Priority",
      "shipmentId": "shp_abc123",
      "rateToken": "rate_abc123"
    }
  ]
}

Pass the chosen id as shippingRateId in POST /checkout. Live rate IDs are prefixed with live: or printful:. freeAbove is the subtotal threshold (in cents) above which this rate becomes free; null means no free-above threshold.


Gift certificates#

POST/gift-certificates/validate#

Validates a gift certificate code and returns the remaining balance. Does not redeem the certificate — that happens at checkout.

  • Auth: Public

Request body

FieldTypeRequiredDescription
codestringYesGift certificate code (case-insensitive)

Response

{
  "success": true,
  "data": {
    "code": "GIFT-ABCD-1234",
    "remainingAmount": 5000,
    "status": "active"
  }
}

Auth#

POST/auth#

Single endpoint for all customer authentication actions. The action field in the request body selects the operation.

  • Auth: Public for all actions (session token required for me and logout)

Common request structure

{ "action": "<action>", ...actionFields }

action: "register"

Requires customer accounts to be enabled for the store (403 otherwise).

FieldTypeRequired
emailstringYes
passwordstringYes (min 8 chars)
firstNamestringNo
lastNamestringNo

Response (201)

{
  "success": true,
  "data": {
    "token": "<session-token>",
    "customer": { "id": 101, "email": "jane@example.com", "firstName": "Jane", "lastName": "Smith" }
  }
}

action: "login"

FieldTypeRequired
emailstringYes
passwordstringYes

Response — same shape as register, 201 becomes 200.

action: "logout"

Invalidates the current session. Pass the token via Authorization: Bearer <token>.

Response{ "success": true }.

action: "me"

Returns the full customer profile for the current session. Requires Authorization: Bearer <token>.

Response — full customer object with id, email, firstName, lastName, phone, defaultShippingAddress, defaultBillingAddress, addressBook, orderCount, totalSpent, createdAt.

action: "forgot-password"

Sends a password-reset email if the address exists. Always returns success to avoid email enumeration.

FieldTypeRequired
emailstringYes

action: "reset-password"

FieldTypeRequired
tokenstringYes
passwordstringYes (min 8 chars)

Account (customer session required)#

All endpoints in this group require Authorization: Bearer <token>. Returns 401 Unauthorized without it.

GET/account#

Returns the authenticated customer's profile. See action: "me" above for the response shape.


PATCH/account#

Updates the customer profile. All fields are optional; only supplied fields are changed.

Request body

FieldTypeDescription
firstNamestring
lastNamestring
phonestring
defaultShippingAddressobjectJSON address object
defaultBillingAddressobjectJSON address object
addressBookarrayArray of saved address objects

GET/account/orders#

Returns all orders placed with the authenticated customer's email address, ordered newest first.

Response

{
  "success": true,
  "data": [
    {
      "id": 9001,
      "orderNumber": "ORD-0042",
      "status": "shipped",
      "paymentStatus": "paid",
      "total": 6380,
      "createdAt": "2026-06-20T08:55:00.000Z"
    }
  ]
}

GET/account/orders/{orderNumber}#

Returns the full order detail including line items (with design thumbnails if applicable), status history, and EasyPost tracking events. Only returns orders that belong to the authenticated customer's email.


GET/account/support#

Lists all support messages submitted by the authenticated customer, ordered by latest activity.


POST/account/support#

Creates a new support message.

Request body

FieldTypeRequiredDescription
subjectstringYesMessage subject
bodystringYesMessage body
categorystringNoCategory tag (default: general)
orderIdintegerNoRelated order ID

Response (201) — the created message record.


GET/account/support/{messageId}#

Returns a support message with its full reply thread.


POST/account/support/{messageId}#

Adds a customer reply to an existing support thread.

Request body

FieldTypeRequired
bodystringYes

GET/account/wishlist#

Returns the customer's default wishlist with enriched product data (name, slug, price, compare-at price, status, first image). Auto-creates the wishlist on first access.


POST/account/wishlist#

Adds a product to the wishlist. Idempotent — if the product is already present the existing item is returned.

Request body

FieldTypeRequired
productIdintegerYes
variantIdintegerNo

DELETE/account/wishlist#

Removes a product from the wishlist.

Request body

FieldTypeRequired
productIdintegerYes

Designs (custom product designer)#

These endpoints power the custom product designer — an in-browser canvas where customers decorate designable products before adding them to the cart.

Auth split: Most design endpoints accept either a customer Bearer token (logged-in customers) or the sd_design_session cookie (anonymous visitors). The cookie is set automatically by POST /designs when a new anonymous session is created. Design ownership is verified against whichever identifier is present.

GET/designs#

Lists designs owned by the current caller (cookie or Bearer). Add ?productId={id} to filter to one product. Add ?templates=1 to return site-wide staff-authored design templates instead.


POST/designs#

Creates a new design for a designable product.

Request body

FieldTypeRequiredDescription
productIdintegerYesMust be a designable product on this site
namestringNoDesign name (default: "Untitled Design")
layersarrayNoInitial layer data

Response (201)

{
  "success": true,
  "data": {
    "id": 77,
    "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "websiteId": 12,
    "productId": 42,
    "name": "My Custom Tee",
    "isPublic": false,
    "createdAt": "2026-06-23T10:00:00.000Z",
    "updatedAt": "2026-06-23T10:00:00.000Z"
  }
}

If the caller has no existing session, the response sets the sd_design_session cookie to establish an anonymous designer session.


GET/designs/{designId}#

Returns a design. The caller must own it (matching customer ID or matching session ID).

{designId} accepts a numeric design ID (new designer, productDesigns table) or a 36-character UUID (legacy designer, designs table).

Query params (GET, UUID designs only)

NameTypeRequired
sessionIdstringWhen using anonymous session without cookie

PUT/designs/{designId}#

Saves design changes.

Request body (numeric ID — new designer)

FieldTypeDescription
layersarrayLayer data array
styleOverridesobjectPer-style overrides
namestringDesign name
descriptionstring
thumbnailUrlstringPreview image URL
styleIdintegerActive style

Request body (UUID — legacy designer)

FieldTypeDescription
namestring
layersBySurfaceobjectKeyed by surface slug
canvasSizeobject{ width, height }
statusstringdraft, finalized, or rendered
sessionIdstringRequired when using anonymous session without cookie

DELETE/designs/{designId}#

Soft-deletes a design (new designer) or hard-deletes it (legacy). The caller must own it.


GET/designs/public/{uuid}#

Returns a publicly shared design. No auth required. Returns 404 if the design exists but isPublic is false.


POST/designs/{designId}/share#

Toggles the public-sharing flag on a design and returns a shareable URL.

Request body

FieldTypeRequired
isPublicbooleanYes

Response

{
  "success": true,
  "design": { ... },
  "shareableUrl": "https://yoursite.com/design/share/a1b2c3d4-...",
  "uuid": "a1b2c3d4-...",
  "isPublic": true
}

POST/designs/{designId}/clone#

Creates a copy of any design the caller can read (owned, public, or template). The clone belongs to the caller.

Request body

FieldTypeDescription
namestringName for the clone (optional)

POST/designs/{designId}/ai-image#

Generates one or more print-ready AI images (via OpenAI gpt-image-1) and uploads them as design assets, ready to drop onto the canvas as image layers. Images are billed against the merchant who owns the website; the per-design rate limit is enforced server-side.

  • Auth: Design ownership required (Bearer or session cookie)

Request body

FieldTypeDefaultDescription
promptstringImage prompt (max 1,000 chars)
stylestringillustrationillustration, photo, graphic, or auto
transparentbooleantrueGenerate transparent-background PNG (recommended for print)
sizestring1024x10241024x1024, 1024x1536, 1536x1024, or auto
qualitystringhighlow, medium, high, or auto
ninteger1Number of variations (1–4)
sessionIdstringPass when using anonymous session without cookie

Response (201)

{
  "success": true,
  "data": {
    "id": 501,
    "url": "https://cdn.example.com/media/designs/77/ai/abc.png",
    "width": 1024,
    "height": 1024,
    "mimeType": "image/png",
    "fileSize": 204800,
    "prompt": "a bold sun rising over mountains",
    "augmentedPrompt": "...",
    "style": "illustration",
    "variants": [
      { "id": 501, "url": "...", "width": 1024, "height": 1024, "mimeType": "image/png", "fileSize": 204800 }
    ]
  }
}

Errors

StatusMessageCause
400Prompt is requiredMissing or blank prompt
402Merchant's AI plan limit reached; upgrade needed
429Per-design rate limit exceeded
503No OpenAI key configured for this merchant

POST/designs/{designId}/ai-text#

Generates short text suggestions for the design canvas (slogans, taglines, copy) using Claude. Billed against the merchant's AI credits.

  • Auth: Design ownership required (Bearer or session cookie)

Request body

FieldTypeRequiredDescription
promptstringYesWhat kind of text to generate (max 600 chars)
productNamestringNoProduct context (max 200 chars)
countintegerNo (default 3)Number of suggestions (1–6)
sessionIdstringNoPass when using anonymous session

Response{ "success": true, "data": { "suggestions": ["text 1", "text 2", ...] } }


POST/designs/{designId}/finalize#

Marks a design as finalized and generates a composite mockup image (artwork composited over the product blank). Called before adding to cart.

  • Auth: Design ownership required

POST/designs/{designId}/assets#

Uploads an image asset to a design (used for customer-uploaded artwork). Returns an asset record with the CDN URL.


POST/designs/claim#

Transfers all anonymous session designs to an authenticated customer after sign-in or registration. Call this once after action: "login" or action: "register" to preserve designs the customer created before logging in.

  • Auth: Bearer token required

Request body

FieldTypeRequiredDescription
sessionIdstringYesThe anonymous sd_design_session value from before login
customerIdintegerYesMust match the authenticated customer's ID

GET/designs/anonymous/count#

Returns the count of designs held under the current anonymous session cookie. Used to show a "you have N saved designs — log in to keep them" prompt.

  • Auth: Public (reads sd_design_session cookie)

GET/designs/fonts#

Returns the list of fonts available in the product designer canvas.

  • Auth: Public

POST/designs/generate-thumbnail#

Generates and stores a flat thumbnail PNG for a design from its layer data. Called by the canvas after significant edits.

  • Auth: Design ownership required

POST/designs/upload-image#

Uploads a customer image (e.g. a photo or logo) for use on the canvas, without tying it to a specific design. Returns a CDN URL.

  • Auth: Design ownership or anonymous session required

POST/designs/assets#

Batch-fetches or registers media assets for the designer. Used internally by the canvas.


Error responses#

All endpoints follow the { success: false, message: string } envelope. Common status codes:

StatusMeaning
400Bad request — missing or invalid parameter
401Unauthorized — missing or invalid Bearer token
403Forbidden — valid session but wrong owner
404Not found — resource or store not found
409Conflict — e.g. email already registered
500Internal server error