Public storefront
The public storefront API is the unauthenticated read path that hosted Storlaunch storefronts use. Anything a buyer can browse without logging in — the merchant's catalogue, individual product pages, the cart-checkout flow, the order tracker — is served from /v1/storefront/public/*.
This is the only section of the API that does not require an API key. Routes are scoped by the merchant's slug (the path segment, e.g. acme), not by your bearer token.
You'll integrate with these endpoints when:
- Building a fully custom (headless) storefront against Storlaunch.
- Embedding a product picker in your own site.
- Implementing a tracking widget against the buyer-facing order URL.
For the auth-required catalogue write API, see Products.
Endpoints
Reads
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/storefront/public/resolve/:slug |
Resolve a slug to its workspace ID + branding |
GET |
/v1/storefront/public/:merchantSlug |
Storefront landing payload (products, branding, hero) |
GET |
/v1/storefront/public/:merchantSlug/:productSlug |
Product detail page |
GET |
/v1/storefront/public/:merchantSlug/blog |
Blog index |
GET |
/v1/storefront/public/:merchantSlug/blog/:slug |
Blog post |
GET |
/v1/storefront/public/:merchantSlug/blog/rss.xml |
Blog RSS feed |
GET |
/v1/storefront/public/:merchantSlug/sitemap |
XML sitemap |
GET |
/v1/storefront/public/:merchantSlug/pixels |
Pixels config (Meta/TikTok/GA) for client init |
GET |
/v1/storefront/public/:merchantSlug/feeds/google.xml |
Google Merchant feed |
GET |
/v1/storefront/public/:merchantSlug/feeds/meta.xml |
Meta catalog feed |
GET |
/v1/storefront/public/:merchantSlug/feeds/tiktok.xml |
TikTok catalog feed |
GET |
/v1/storefront/public/:merchantSlug/discount-codes |
Public-listed discount codes |
GET |
/v1/storefront/public/:merchantSlug/r/:code |
Resolve a referral link |
GET |
/v1/storefront/public/order/:deliveryId |
Buyer-facing order page (download URL, tracking) |
GET |
/v1/storefront/public/:merchantSlug/order/:number |
Order tracker by order number |
Writes (unauthenticated, but rate-limited and CAPTCHA-gated)
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/storefront/public/track |
Submit a pixel/analytics event |
POST |
/v1/storefront/public/:merchantSlug/:productSlug/checkout |
Single-product buy-now checkout |
POST |
/v1/storefront/public/:merchantSlug/cart-checkout |
Multi-product cart checkout |
POST |
/v1/storefront/public/:merchantSlug/manual-checkout |
Manual-payment (bank transfer / WhatsApp) checkout |
POST |
/v1/storefront/public/validate-discount |
Quote a discount code against a cart |
POST |
/v1/storefront/public/:merchantSlug/referral/capture |
Capture a referral attribution cookie |
GET/POST |
/v1/storefront/public/abandoned-cart/unsubscribe |
Unsubscribe link target |
Storefront landing
GET /v1/storefront/public/:merchantSlug
Returns the everything-the-homepage-needs payload: merchant branding, the published product list, the featured/hero block, and the active discount-codes banner (public-listed only).
Path parameters
| Param | Type | Description |
|---|---|---|
merchantSlug |
string | The merchant's account.slug (or a known custom-domain alias). |
Response — 200 OK
{
"data": {
"merchant": {
"id": "acc_01HX...",
"name": "Acme",
"slug": "acme",
"brandColor": "#0a84ff",
"brandLogo": "https://cdn.../logo.png",
"brandBanner": "https://cdn.../banner.jpg",
"description": "Independent stationery, made in Bandung."
},
"products": [ /* product objects, published only, archived excluded */ ],
"discountCodes": [ /* publicly-listed codes only */ ],
"pixels": { "metaPixelId": "...", "tiktokPixelId": "...", "gaMeasurementId": "..." }
},
"error": null,
"meta": { "requestId": "req_01HX...", "timestamp": "2026-05-13T10:42:00Z" }
}
Errors
| Status | error.code |
When |
|---|---|---|
404 |
RESOURCE_NOT_FOUND |
No merchant has this slug (and no custom-domain alias matches). |
Product detail
GET /v1/storefront/public/:merchantSlug/:productSlug
Returns one product plus its variants, attached files (digital only), inventory state (when fulfillment module on), and the resolved cross-sell list. Used to render the product detail page.
curl https://storlaunch.forjio.com/api/v1/storefront/public/acme/field-notes-notebook
Buy-now checkout
POST /v1/storefront/public/:merchantSlug/:productSlug/checkout
Creates a checkout session for a single product purchase. Returns a hosted URL the buyer is redirected to. The upstream payment is handled by Plugipay; Storlaunch only owns the session orchestration.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | Buyer email — required for digital delivery and receipts. |
name |
string | no | Buyer's display name. |
phone |
string | no | Phone, for couriers that need it. |
variantId |
string | no | The chosen variant (if the product has them). |
quantity |
integer | no | Default 1. |
shippingAddress |
object | conditional | Required for physical products. See address shape. |
couponCode |
string | no | Apply a discount code at the create stage. |
referralCode |
string | no | Referral attribution. |
successUrl / cancelUrl |
string | no | Override the post-pay redirects. |
metadata |
object | no | Free-form passthrough into the checkout session and the downstream product.purchased event. |
Response — 201 Created
{
"data": {
"checkoutSessionId": "cs_01HX...",
"hostedUrl": "https://pay.plugipay.com/c/cs_01HX..."
},
"error": null,
"meta": { "requestId": "req_01HX...", "timestamp": "2026-05-13T10:42:00Z" }
}
Redirect the buyer to hostedUrl immediately. After payment, the post-pay flow runs and checkout.completed + product.purchased events fire.
Cart checkout
POST /v1/storefront/public/:merchantSlug/cart-checkout
Same as buy-now but accepts an array of { productId, variantId?, quantity } items. Returns one combined hostedUrl; on payment, one delivery row is created per line item.
Manual checkout
POST /v1/storefront/public/:merchantSlug/manual-checkout
For workspaces on the free tier (no Plugipay module). Records a "manual order" with status awaiting_payment and emails the buyer the merchant's bank account details / WhatsApp number. The merchant later confirms payment in the dashboard, which flips the order to payment_confirmed and starts fulfilment.
This endpoint does not create a cs_… checkout session — manual orders live in a separate ManualOrder table; see Manual orders.
Validate a discount
POST /v1/storefront/public/validate-discount
Quote a discount code against a cart without creating a checkout. Returns the discount value and any error reason (CODE_NOT_FOUND, MIN_PURCHASE_NOT_MET, MAX_USES_REACHED, EXPIRED). Used by storefronts to give live feedback in the cart UI.
// Request
{
"code": "LAUNCH10",
"merchantSlug": "acme",
"items": [
{ "productId": "prod_01HX...", "variantId": "var_01HX...", "quantity": 2 }
],
"subtotalAmount": 150000
}
// Response (success)
{
"data": {
"valid": true,
"code": "LAUNCH10",
"type": "percent",
"value": 10,
"discountAmount": 15000,
"appliesToShipping": false
},
"error": null,
"meta": { "requestId": "req_01HX...", "timestamp": "2026-05-13T10:42:00Z" }
}
Order tracking
GET /v1/storefront/public/order/:deliveryId
The buyer's post-purchase URL. Returns the delivery object without auth — the del_ ID itself is the credential (random 26-char ULID, infeasible to guess). Renders the download button, the licence key, or the courier tracking widget.
The merchant-scoped sibling, /:merchantSlug/order/:number, takes a human-readable order number from the post-pay receipt email instead of the raw ID.
Custom domains
Custom merchant domains (e.g. shop.acme.com) resolve to the same endpoints via the /resolve/:slug lookup. The host header is matched against a Domain row; if it matches, the request is rewritten to the corresponding merchant slug before the handler runs. See Domains for set-up.
Rate limits
Public endpoints share a per-IP rate budget separate from the authenticated tier:
| Endpoint class | Limit |
|---|---|
| Read (catalogue, blog, feeds) | 200 req/min per IP |
| Checkout creates | 10 req/min per IP per merchant |
| Validate discount | 30 req/min per IP per merchant |
Hitting the cap returns 429 Too Many Requests. The hosted storefront and SDKs already respect these — you're unlikely to hit them outside scripted abuse.
Events
The public endpoints don't emit events directly. Downstream events are emitted when state transitions happen:
- A successful checkout-create kicks off a Plugipay checkout flow that, on payment, emits
checkout.completedandproduct.purchased. - A manual checkout writes a row, no event yet — the event chain starts when the merchant confirms in the dashboard.
Next
- Products — the catalogue surfaced by these reads.
- Manual orders — how manual checkouts land in the dashboard.
- Discount codes — what
validate-discountresolves. - Deliveries — the row backing the order-tracker page.