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.

Response201 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.

Response200 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.

Response200 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.

Response200 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).

Response204 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 (generatingcomplete 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.