Event catalogue
Every event Sellvik can deliver, with payload shapes.
Envelope
Every webhook delivery uses the same envelope:
{
"id": "evt_a1b2c3d4e5f6...",
"type": "order.created",
"createdAt": "2026-05-27T13:45:00.000Z",
"shopSubdomain": "acme",
"shopId": "sh_a1b2c3...",
"data": { /* event-specific */ }
}| Field | Description |
|---|---|
id | Unique event ID. Use for idempotency. |
type | Event name. Stable across versions. |
createdAt | When the event fired server-side (not when it was delivered). |
shopSubdomain | Shop slug — useful for routing in multi-shop integrations. |
shopId | Shop UUID — stable across subdomain changes. |
data | Event-specific. See sections below. |
Idempotency
The same id may be delivered more than once (at-least-once semantics; see
Delivery and retries). Deduplicate by
storing recent ids and dropping replays.
async function handle(event: WebhookEvent) {
const seen = await redis.set(`webhook:${event.id}`, "1", { NX: true, EX: 86400 * 7 })
if (!seen) return // already processed
switch (event.type) { /* … */ }
}Order events
order.created
Fired when a customer completes checkout. The order is PENDING (or
CONFIRMED for non-COD payment methods).
{
"type": "order.created",
"data": {
"order": {
"id": "ord_a1b2c3...",
"orderNumber": "ACME-1042",
"status": "PENDING",
"subtotal": "2980.00",
"shippingCost": "60.00",
"discountAmount": "0.00",
"total": "3040.00",
"paymentMethod": "COD",
"customer": {
"id": "u1b2...",
"name": "Rafiul Hassan",
"email": "rafiul@example.com",
"phoneNumber": "+8801711000000"
},
"shippingAddress": { "name": "...", "phone": "...", "addressLine1": "...", "city": "Dhaka", "district": "Dhaka", "country": "BD" },
"items": [
{ "productId": "...", "name": "Emerald Kurti", "quantity": 2, "price": "1490.00", "total": "2980.00" }
],
"createdAt": "2026-05-27T13:45:00.000Z"
}
}
}order.updated
Fired when an order's metadata changes (customer note added, address edited before shipping).
{
"type": "order.updated",
"data": {
"orderId": "ord_a1b2c3...",
"changes": ["customerNote", "shippingAddress"],
"order": { /* full order object */ }
}
}order.status_changed
Fired on every status transition. The from and to fields are the
previous and new values from the order lifecycle.
{
"type": "order.status_changed",
"data": {
"orderId": "ord_a1b2c3...",
"from": "PENDING",
"to": "CONFIRMED",
"changedAt": "2026-05-27T14:00:00.000Z",
"changedBy": "user_a1b2..."
}
}Convenience derived events also fire alongside this one for the common transitions:
order.confirmed—to: "CONFIRMED"order.shipped—to: "SHIPPED"order.delivered—to: "DELIVERED"order.cancelled—to: "CANCELLED"order.refunded—to: "REFUNDED"order.disputed—to: "DISPUTED"order.on_hold—to: "ON_HOLD"
Subscribe to whichever fits your handler best. Use order.status_changed
if you want the full transition log.
order.fulfilled
Fired when a courier is assigned and tracking info is generated. Includes the courier's tracking ID.
{
"type": "order.fulfilled",
"data": {
"orderId": "ord_a1b2c3...",
"courier": "STEADFAST",
"trackingId": "STF-12345",
"trackingUrl": "https://steadfast.com.bd/track/STF-12345",
"fulfilledAt": "2026-05-27T15:00:00.000Z"
}
}Inventory events
inventory.adjusted
Fired on every POST /v1/admin/inventory/adjust call (manual or via API)
and on every checkout that decrements stock.
{
"type": "inventory.adjusted",
"data": {
"productId": "f3a7...",
"variantId": null,
"delta": -2,
"previousStock": 12,
"newStock": 10,
"reason": "order:ord_a1b2c3...",
"source": "checkout"
}
}inventory.low_stock
Fired once per crossing when a product or variant's stock drops at or below
its lowStockThreshold (set per product; default 5).
{
"type": "inventory.low_stock",
"data": {
"productId": "f3a7...",
"variantId": null,
"stock": 4,
"threshold": 5
}
}Not re-fired until stock goes above the threshold and back below it.
inventory.out_of_stock
Fired when stock hits exactly zero.
{
"type": "inventory.out_of_stock",
"data": {
"productId": "f3a7...",
"variantId": null
}
}Product events
product.created
{
"type": "product.created",
"data": { "product": { /* full Product object (admin shape) */ } }
}product.updated
{
"type": "product.updated",
"data": {
"productId": "f3a7...",
"changes": ["price", "comparePrice"],
"product": { /* full product */ }
}
}product.deleted
{
"type": "product.deleted",
"data": { "productId": "f3a7..." }
}Customer events
customer.created
{
"type": "customer.created",
"data": { "customer": { "id": "u1b2...", "email": "...", "name": "...", "phoneNumber": "..." } }
}customer.updated
{
"type": "customer.updated",
"data": {
"customerId": "u1b2...",
"changes": ["phoneNumber"],
"customer": { /* full customer */ }
}
}Cart events
cart.abandoned
Fired by the abandoned-cart cron when a cart has been idle for ≥4 hours and contains items.
{
"type": "cart.abandoned",
"data": {
"cartId": "ca1b2...",
"customerId": "u1b2...",
"subtotal": "2980.00",
"items": [/* line items */],
"lastActivityAt": "2026-05-27T09:00:00.000Z"
}
}Webhook administration events
webhook.failed
Meta-event fired when a webhook delivery exhausts retries. Lets you build a "webhooks are broken" alert.
{
"type": "webhook.failed",
"data": {
"webhookId": "wh_a1b2...",
"deliveryId": "del_a1b2...",
"originalEvent": "order.created",
"lastResponseCode": 502,
"attempts": 8
}
}Subscribe a different webhook to this event — subscribing the failing webhook to its own failures defeats the purpose.
Event name stability
Event names (order.created, inventory.adjusted, etc.) are part of the
v1 contract and will never be renamed. New events ship without notice;
field shapes within an event payload may add fields (additive) but never
rename or remove them in v1.