Products
A product is anything you sell through Storlaunch. Its type is one of three: physical (a mug, a t-shirt), digital (a PDF, a Figma file), or subscription (recurring access). Licence keys are not a type — set licenseEnabled on any product to issue one per purchase. Products are the root of the catalogue — everything else (variants, files, deliveries, orders) attaches to one.
Every product belongs to exactly one workspace (accountId). The same product can be sold through the merchant's hosted storefront, the public API, and (when the fulfilment module is enabled) mirrored into Fulkruma for inventory and shipping orchestration.
All requests on this page require an sk_* key — publishable pk_* keys cannot write the catalogue. See Authentication for the bearer scheme, and API overview for the response envelope and pagination convention.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/storefront/products |
Create a product |
GET |
/v1/storefront/products |
List products |
GET |
/v1/storefront/products/:id |
Retrieve one product |
PATCH |
/v1/storefront/products/:id |
Update a product |
DELETE |
/v1/storefront/products/:id |
Archive a product (soft-delete) |
POST |
/v1/storefront/products/:id/ai-generate |
Re-run AI description generation |
For file management under a product, see Product files. For licence keys, see Licences.
Create a product
POST /v1/storefront/products
Creates a product in the workspace your API key belongs to. The minimum viable body is { "name": "...", "price": 1000, "currency": "IDR", "type": "physical" }; everything else is optional and defaults to safe values.
If slug is omitted, Storlaunch derives one from name (lowercase, hyphen-separated, non-alnum stripped). If the derived slug collides with an existing product in the same workspace, we suffix a base-36 timestamp.
Tier-limited. Free and Starter plans cap product count. A 403 QUOTA_EXCEEDED is returned when you exceed your tier's limit; upgrade or archive an existing product to free a slot.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string (1–200) | yes | Display name on the storefront. |
price |
integer | yes | Positive integer in whole units of currency — 150000 with IDR is Rp 150.000, 25 with USD is $25. Never minor units, never cents. |
currency |
string (3) | yes | ISO 4217 code. IDR or USD. |
type |
digital | subscription | physical |
yes | Drives fulfilment behaviour. digital enables product-files; physical requires weight and enables shipping. There is no license type — licence issuance is the licenseEnabled flag below, and works on any type. |
slug |
string (2–80, ^[a-z0-9-]+$) |
no | URL-safe identifier. Derived from name if omitted. Unique per workspace. |
description |
string (≤10 000) | no | Markdown-rendered on the storefront. |
thumbnail |
string (URL) | no | Primary image. |
coverImages |
array of strings (URL) | no | Additional images, ordered. |
tags |
array of strings | no | Free-form tags. Used by discount codes for tag-scoped lookups. |
aiGenerate |
boolean | no | When true, queues an async job to fill description + thumbnail via Storlaunch's AI generator. Quota-limited per plan. |
licenseEnabled |
boolean | no | Generate a licence key on each purchase. Independent of type. |
maxActivations |
integer | no | Max activations per generated licence. Default 1. |
weight / length / width / height |
integer | no | Physical dimensions (weight in grams, the rest in cm) for shipping rate quotes. weight is required when type is physical — omitting it returns 400 VALIDATION_ERROR on weight. |
madeToOrder |
boolean | no | Pre-order / made-to-order. Default false. |
preorderShipsFrom |
string (ISO 8601) | no | Earliest ship date shown to buyers on a pre-order product. |
leadTimeText |
string (≤500) | no | Free-text lead time, e.g. "ships in 2–3 weeks". |
batchHint |
string (≤500) | no | Free-text batch note, e.g. "next batch closes 20 Sep". |
originAreaId |
string | no | Override the workspace's default shipping origin. |
requiresInsurance |
boolean | no | Forces couriers that offer insurance. |
gtin / googleProductCategory / brand |
string | no | Google Merchant Center feed fields. |
feedExcluded |
boolean | no | Hide from /feeds/google.xml, /feeds/meta.xml, /feeds/tiktok.xml. |
published |
boolean | no | Show on the storefront. Default false — new products are drafts. |
metadata |
object | no | Free-form string-to-string map. Surfaced in webhooks. |
Unknown fields are rejected with 400 VALIDATION_ERROR. Don't set id, accountId, archived, or timestamps — those are server-managed.
Response — 201 Created
{
"data": {
"id": "prod_01HXAB7K3M9N2P5QRS8TVWXY3Z",
"name": "Field Notes Notebook",
"slug": "field-notes-notebook",
"description": "Pocket-sized, dot-grid, made in Bandung.",
"price": 75000,
"currency": "IDR",
"type": "physical",
"thumbnail": "https://cdn.storlaunch.com/u/abc.jpg",
"coverImages": [],
"tags": ["stationery", "indonesian-made"],
"aiGenerated": false,
"aiStatus": null,
"licenseEnabled": false,
"maxActivations": 1,
"weight": 120,
"length": 140,
"width": 90,
"height": 12,
"originAreaId": null,
"requiresInsurance": false,
"gtin": null,
"googleProductCategory": null,
"brand": null,
"feedExcluded": false,
"published": false,
"archived": false,
"pageUrl": "https://storlaunch.forjio.com/s/acme/field-notes-notebook",
"files": [],
"metadata": {},
"createdAt": "2026-05-13T10:42:00.123Z",
"updatedAt": "2026-05-13T10:42:00.123Z"
},
"error": null,
"meta": { "requestId": "req_01HX...", "timestamp": "2026-05-13T10:42:00Z" }
}
pageUrl is the public storefront URL the buyer hits. null if the workspace has no slug set yet.
Errors specific to this endpoint
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION_ERROR |
Required field missing, unknown field present, shape wrong (type not in enum, currency not IDR/USD, price not a positive integer, weight missing on a physical product). |
403 |
QUOTA_EXCEEDED |
The workspace is at its product cap for the current plan, or AI generation quota is exhausted. |
401 |
UNAUTHORIZED |
Key missing, malformed, or revoked. |
403 |
FORBIDDEN |
Key is pk_* (publishable keys can't create). |
Other 4xx/5xx errors follow Errors.
Examples
// Node
import { Storlaunch } from '@forjio/storlaunch';
const client = new Storlaunch({ apiKey: process.env.STORLAUNCH_API_KEY });
const product = await client.products.create({
name: 'Field Notes Notebook',
price: 75000,
currency: 'IDR',
type: 'physical',
tags: ['stationery'],
});
# Python
from storlaunch import Storlaunch
client = Storlaunch(api_key=os.environ['STORLAUNCH_API_KEY'])
product = client.products.create(
name='Field Notes Notebook',
price=75000,
currency='IDR',
type='physical',
tags=['stationery'],
)
// Go
import storlaunch "github.com/hachimi-cat/storlaunch-go"
client := storlaunch.New(os.Getenv("STORLAUNCH_API_KEY"))
product, err := client.Products.Create(ctx, &storlaunch.ProductCreateParams{
Name: "Field Notes Notebook",
Price: 75000,
Currency: "IDR",
Type: "physical",
Tags: []string{"stationery"},
})
# curl
curl -X POST https://storlaunch.forjio.com/api/v1/storefront/products \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"name":"Field Notes Notebook","price":75000,"currency":"IDR","type":"physical"}'
Retrieve a product
GET /v1/storefront/products/:id
Returns one product, including attached files. The id must include the prod_ prefix.
Path parameters
| Param | Type | Description |
|---|---|---|
id |
string (prod_…) |
Product ID. |
Response — 200 OK. The same shape as the product object.
Errors
| Status | error.code |
When |
|---|---|---|
404 |
RESOURCE_NOT_FOUND |
Product doesn't exist, or exists in another workspace. |
curl https://storlaunch.forjio.com/api/v1/storefront/products/prod_01HX... \
-H "Authorization: Bearer sk_live_xxx"
List products
GET /v1/storefront/products
Returns non-archived products in the workspace, newest first. Cursor-paginated — see Pagination.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 |
Page size, clamped to [1, 100]. |
cursor |
string | — | Opaque cursor from a previous meta.page.nextCursor. |
published |
boolean | — | If set, filters by publish state. |
type |
digital | subscription | physical |
— | Filter by product type. |
There's no name- or tag-substring filter. Pull pages and filter client-side if you need that.
Response — 200 OK. Paginated envelope; data is an array of product objects.
// Node — auto-paginate to end
for await (const p of client.products.list({ limit: 100 })) {
await handle(p);
}
# Python
for p in client.products.list(limit=100).auto_paging_iter():
handle(p)
curl 'https://storlaunch.forjio.com/api/v1/storefront/products?limit=50&published=true' \
-H "Authorization: Bearer sk_live_xxx"
Update a product
PATCH /v1/storefront/products/:id
Partial update — send only the fields you want to change. Omitted fields are left untouched. The slug is not patchable via this endpoint; rename via the dashboard or contact support.
Request body — same shape as Create, all fields optional:
| Field | Type | Description |
|---|---|---|
name |
string | Rename. |
description |
string | Replace the description. |
price |
integer | Change the price (positive integer, whole units of the product's currency). |
thumbnail |
string | Swap the primary image. |
coverImages |
array | Replace the cover-image array. |
tags |
array | Replace the tag array. |
published |
boolean | Publish or unpublish. |
licenseEnabled / maxActivations |
boolean / integer | Licence settings. Independent of type. |
gtin / googleProductCategory / brand / feedExcluded |
string / boolean | Merchant-Center feed fields. |
metadata |
object | Replace metadata map. |
Response — 200 OK. The full updated product object.
Errors
| Status | error.code |
When |
|---|---|---|
404 |
RESOURCE_NOT_FOUND |
Product doesn't exist or is in another workspace. |
400 |
VALIDATION_ERROR |
Shape wrong or unknown field. |
// Node
await client.products.update('prod_01HX...', { price: 80000, published: true });
Archive a product
DELETE /v1/storefront/products/:id
Soft-delete — sets archived: true and published: false. The row stays; existing orders and licences remain queryable. Archived products are hidden from GET /v1/storefront/products by default.
There's no hard-delete endpoint, on purpose: cascading through orders, deliveries, licences, and ledger entries would lose history. To restore, PATCH with archived: false (currently only supported via the dashboard).
Response — 204 No Content.
Errors
| Status | error.code |
When |
|---|---|---|
404 |
RESOURCE_NOT_FOUND |
Product doesn't exist or is in another workspace. |
Regenerate AI assets
POST /v1/storefront/products/:id/ai-generate
Queues a fresh AI generation pass over the product's copy: it rewrites description and stores an SEO title, meta description and feature bullets in metadata. It does not generate images — thumbnail and coverImages are never touched. Used when the first pass produced a result you don't like. Returns 202 Accepted with a { message: "AI generation started" } body — the work happens off-thread; poll the product for aiStatus transitions (generating → completed or failed).
Quota-limited per plan. 403 QUOTA_EXCEEDED once the monthly AI count is exhausted.
The product object
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
string | no | Always prod_ + 26-char ULID. |
name |
string | no | Display name. |
slug |
string | no | URL-safe slug, unique per workspace. |
description |
string | yes | Markdown body. |
price |
integer | no | Price in whole units of currency. |
currency |
string | no | ISO 4217. |
type |
enum | no | digital / subscription / physical. |
thumbnail |
string | yes | Primary image URL. |
coverImages |
array | no | Additional image URLs. |
tags |
array | no | String tag list. |
aiGenerated |
boolean | no | true if AI fill was triggered at some point. |
aiStatus |
enum | yes | null if never used, otherwise generating / completed / failed. |
licenseEnabled |
boolean | no | Issue licences on purchase. |
maxActivations |
integer | no | Activations per licence. |
weight / length / width / height |
integer | yes | Physical dims (weight g, the rest cm). |
originAreaId |
string | yes | Shipping origin override. |
requiresInsurance |
boolean | no | Force insured couriers. |
gtin / googleProductCategory / brand |
string | yes | Merchant-Center fields. |
feedExcluded |
boolean | no | Hide from product feeds. |
published |
boolean | no | Visible on storefront. |
archived |
boolean | no | Soft-deleted. |
pageUrl |
string | yes | Public storefront URL (null if workspace has no slug). |
files |
array | no | Attached product-files records. Empty array for non-digital products. |
metadata |
object | no | String-to-string map. |
createdAt / updatedAt |
string (ISO 8601 UTC) | no | Timestamps. |
The same shape is returned everywhere — create response, list items, retrieve, update, and inside webhook payloads.
Events
Every successful product mutation fans out to your registered webhook endpoints via the outbox. Payloads use the canonical product shape above.
| Event type | Fires on | Notes |
|---|---|---|
storlaunch.product.created.v1 |
POST /v1/storefront/products succeeds. |
Also emitted to Fulkruma when the fulfilment module is enabled. |
storlaunch.product.updated.v1 |
PATCH /v1/storefront/products/:id succeeds. |
Sent on every successful patch, even no-op ones. |
storlaunch.product.archived.v1 |
DELETE /v1/storefront/products/:id. |
Payload is { id, accountId }, not the full product. |
storlaunch.variant.created.v1 |
A variant is added under this product. | Variants live behind /v1/inventory/variants. |
storlaunch.variant.updated.v1 |
A variant is updated. | Payload absolute priceCents — storlaunch's priceDelta is pre-resolved against the parent product. |
storlaunch.variant.archived.v1 |
A variant is deleted. | Payload { id, productId }. |
Fan-out is fire-and-forget — a downstream outage never blocks the API call. Delivery is at-least-once; dedupe on event.id in your handler.
See Webhooks for the envelope, signature recipe, and retry policy.
Next
- Product files — attach downloadable files to digital products.
- Licences — issue and validate licence keys for products with
licenseEnabled. - Inventory — warehouses and stock per variant.
- Public storefront — how buyers see and purchase these products.
- Webhooks — subscribe to product events.