storlaunch.product.created.v1

Fires when a product is created in the workspace — either via POST /v1/storefront/products, the dashboard's Products → New action, or the AI-quick-add flow. The event reflects the state at the moment the row was committed; subsequent edits arrive as storlaunch.product.updated.v1.

Subscribe if you mirror the catalogue into another system (Fulkruma, Ripllo, your own search index, a price-comparison feed). Storlaunch already wires Fulkruma + Ripllo system endpoints to this event when their modules are on; you don't need to handle the same fan-out yourself unless you have a third destination.

When it fires

In every code path that writes a Product row with archived: false:

  1. POST /v1/storefront/products succeeds.
  2. The dashboard's quick-create or AI generator commits a product.
  3. The CSV-import path inserts a row.

Fire is fire-and-forget — the API call returns 201 before the outbox flushes. Delivery is at-least-once; dedupe on event.id.

Payload

{
  "id": "evt_01HX...",
  "type": "storlaunch.product.created.v1",
  "createdAt": "2026-05-13T10:42:00Z",
  "accountId": "acc_01HX...",
  "data": {
    "id": "prod_01HX...",
    "accountId": "acc_01HX...",
    "name": "Field Notes Notebook",
    "slug": "field-notes-notebook",
    "sku": null,
    "description": "Pocket-sized, dot-grid, made in Bandung.",
    "type": "physical",
    "weight": 120,
    "length": 140,
    "width": 90,
    "height": 12,
    "licenseEnabled": false,
    "maxActivations": 1,
    "archived": false,
    "priceCents": 75000,
    "currency": "IDR",
    "imageUrl": "https://cdn.../thumb.jpg",
    "available": false,
    "tags": ["stationery"],
    "brand": null,
    "googleProductCategory": null
  }
}

Note: priceCents is the smallest-unit price (IDR has no fractional unit, so this is just rupiah). available is the derived "published && !archived" flag.

Handler examples

// Node
import { verifyWebhook } from '@forjio/storlaunch/webhooks';

app.post('/webhooks/storlaunch', async (req, res) => {
  const event = verifyWebhook(req.rawBody, req.headers['x-storlaunch-signature'], process.env.STORLAUNCH_WEBHOOK_SECRET);
  if (event.type === 'storlaunch.product.created.v1') {
    const p = event.data;
    await searchIndex.upsert({ id: p.id, name: p.name, tags: p.tags, priceCents: p.priceCents });
  }
  res.status(200).end();
});
# Python
from storlaunch.webhooks import verify_webhook

@app.post("/webhooks/storlaunch")
def handle(req):
    event = verify_webhook(req.body, req.headers["x-storlaunch-signature"], WEBHOOK_SECRET)
    if event["type"] == "storlaunch.product.created.v1":
        p = event["data"]
        search_index.upsert(id=p["id"], name=p["name"], tags=p["tags"], price_cents=p["priceCents"])
    return ("", 200)
// Go
event, err := storlaunch.VerifyWebhook(body, r.Header.Get("X-Storlaunch-Signature"), secret)
if err != nil { http.Error(w, "bad sig", 401); return }
if event.Type == "storlaunch.product.created.v1" {
    var p storlaunch.Product
    _ = json.Unmarshal(event.Data, &p)
    search.Upsert(ctx, p.ID, p.Name, p.Tags, p.PriceCents)
}
w.WriteHeader(200)

What to do

  • Mirror the row into your search index, ERP, or analytics warehouse.
  • If available: true, queue any "new product" buyer notifications.
  • Tag the row in your CRM if metadata carries a campaign source.

Common pitfalls

  • Treating available: false as "archived". available is published && !archived. A false product just hasn't been put live yet.
  • Assuming imageUrl is permanent. Thumbnails on Storlaunch's CDN may be re-signed/migrated. Re-resolve from the payload on each read rather than caching the URL.
  • Skipping dedupe. Outbox is at-least-once. Use event.id as a unique key in your processed-events table.
  • Reacting to your own writes. If your CRM also creates products via API, guard against round-trips with a metadata.source tag.

Related events

Next