product.purchased
Fires once per delivery created by the post-payment fulfilment flow — the moment a buyer successfully completes a checkout for a Storlaunch product and the delivery row is written. This is the canonical "fulfil this" event for Storlaunch merchants: digital downloads, licence keys, and physical-shipment kick-offs all run through it.
One purchase, one fulfilment. For multi-product cart checkouts, you receive one event per line item (each with its own deliveryId) referencing the same sessionId.
When it fires
In the fulfilCheckoutSession server-side flow, after:
- The checkout session transitioned to
completed(via Plugipay webhook ingestion, manual confirmation, or PayPal capture). - The delivery row was successfully written for this product line.
- For physical products, the Fulkruma shipment create succeeded (or failed-non-blocking).
It does not fire if any of those steps errored — partial fulfilment leaves the line in pending_fulfilment and emits no event. Use GET /v1/storefront/deliveries to reconcile if you suspect drops.
Payload
{
"id": "evt_01HX...",
"type": "product.purchased",
"createdAt": "2026-05-13T10:43:22Z",
"accountId": "acc_01HX...",
"data": {
"deliveryId": "del_01HX...",
"productId": "prod_01HX...",
"sessionId": "cs_01HX...",
"licenseKey": "STORLAUNCH-AB12-CD34-EF56-GH78",
"hasShipping": false
}
}
deliveryId— the row in Deliveries. HitGET /v1/storefront/deliveries/:idfor download URL + expiry.productId— the product that was purchased.sessionId— thecs_…checkout session. Use it to dedupe and to look up buyer metadata (customerEmail,metadata.orderId).licenseKey— only set fortype: licenseproducts.nullfor digital downloads and physical goods.hasShipping—truefor physical products. Means a Fulkruma shipment has been created and the buyer will receive courier emails.
The payload deliberately stays slim; fetch the delivery (or session, or product) by ID for the full picture.
Handler examples
// Node
import { verifyWebhook } from '@forjio/storlaunch/webhooks';
import { Storlaunch } from '@forjio/storlaunch';
const client = new Storlaunch({ apiKey: process.env.STORLAUNCH_API_KEY });
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 === 'product.purchased') {
const { deliveryId, productId, sessionId } = event.data;
const delivery = await client.deliveries.retrieve(deliveryId);
await crm.orders.markPaid(sessionId, { deliveryId, downloadUrl: delivery.downloadUrl });
await analytics.track('Purchase', { productId, sessionId, value: delivery.amount });
}
res.status(200).end();
});
# Python
if event["type"] == "product.purchased":
d = event["data"]
delivery = client.deliveries.retrieve(d["deliveryId"])
crm.orders.mark_paid(d["sessionId"], delivery_id=d["deliveryId"], download_url=delivery.get("downloadUrl"))
analytics.track("Purchase", product_id=d["productId"], session_id=d["sessionId"], value=delivery["amount"])
// Go
if event.Type == "product.purchased" {
var d struct {
DeliveryID string `json:"deliveryId"`
ProductID string `json:"productId"`
SessionID string `json:"sessionId"`
HasShipping bool `json:"hasShipping"`
}
_ = json.Unmarshal(event.Data, &d)
delivery, _ := client.Deliveries.Retrieve(ctx, d.DeliveryID)
crm.MarkPaid(ctx, d.SessionID, delivery)
}
What to do
- Provision. Grant the buyer access to whatever they bought — this is the moment to flip their license, send your own welcome email, or ping fulfilment Slack.
- Update analytics. Send the
Purchaseevent to Meta CAPI / TikTok / GA. Storlaunch already does this server-side via the pixel-cookies stashed on the session, but if you run your own pipeline, fire here. - Dedupe. Use
event.idas your event-store key. Receiving the same event twice is normal under at-least-once delivery.
Common pitfalls
- Treating
product.purchasedas full order data. It's a notification, not a payload. Fetch the delivery or session for amount, customer email, and metadata. - Doing the work twice. Webhooks are at-least-once; dedupe before fulfilling. A double-fire causing a double-deliver of a download or shipment is a really bad bug.
- Ignoring multi-product carts. A cart with 3 line items fires 3 events. Don't assume one per session.
- Skipping
hasShipping. Whentrue, the buyer is about to receive courier emails from Fulkruma — don't also send your own "your order has shipped" message until you have the actual tracking number (subscribe on Fulkruma side for that). - Caching the download URL forever. The delivery's
downloadUrlis a time-limited signed URL. Re-resolve each time the buyer asks.
Related events
checkout.completed— fires once per session, before fulfilment. Use it for the "money in" hook.