Products
A product is anything you sell through Storlaunch: a physical good (a mug, a t-shirt), a digital download (a PDF, a Figma file), or a licence key (an app activation). 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 cover/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 | Price in the currency's smallest unit (cents for USD, rupiah for IDR — IDR has no subdivision). |
currency |
string (3) | yes | ISO 4217 code. Currently IDR, USD, SGD. |
type |
physical | digital | license |
yes | Drives fulfilment behaviour. digital enables product-files; license enables licence generation. |
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 | (For type: license.) Generate a licence key on each purchase. |
maxActivations |
integer | no | Max activations per generated licence. Default 1. |
weight / length / width / height |
integer | no | Physical dimensions (grams / mm) for shipping rate quotes. |
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 3 chars). |
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/saas-storlaunch/sdk/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 |
physical | digital | license |
— | 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 (in smallest currency unit). |
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 (license-type only). |
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 for description, thumbnail, or both. 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 → complete 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 | Smallest-unit price in currency. |
currency |
string | no | ISO 4217. |
type |
enum | no | physical / digital / license. |
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 / complete / failed. |
licenseEnabled |
boolean | no | Issue licences on purchase. |
maxActivations |
integer | no | Activations per licence. |
weight / length / width / height |
integer | yes | Physical dims (g, mm). |
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 license-type products.
- Inventory — warehouses and stock per variant.
- Public storefront — how buyers see and purchase these products.
- Webhooks — subscribe to product events.