Shop · Integrate
Events and webhooks
Typed, versioned commerce events for plugins, delivered to other systems as signed webhooks.
Shop records every commerce change in a site-scoped outbox, in the same transaction as the change. A per-site job emits them after commit, and reconciliation catches missed jobs, so a listener failure can never undo a sale. Delivery is at least once.
Events
Every name starts with justflows.shop.. Every payload has version: 1, a stable eventKey, and occurredAt (ISO UTC).
| Event | Fields | When |
|---|---|---|
order.created | order, source; optional subscriptionId, period | Checkout, a renewal order, a staff assignment, or a default plan |
order.paid | order | The payment is fully captured |
order.payment_failed | order, nullable paymentId | An online payment failed, was cancelled, or expired |
order.cancelled | order, previousStatus | Staff saved a cancellation |
order.refunded | order, refund | A local or external refund was recorded |
order.shipped | order, shipment | Some or all lines shipped |
order.delivered | order | Tracking showed every shipment delivered |
subscription.started | subscription, change | Started after payment, or without one |
subscription.renewed | subscription, change | A renewal was paid |
subscription.payment_failed | subscription, change | A subscription payment failed |
subscription.updated | subscription, change | Paused, resumed, suspended, or a cancellation was scheduled or withdrawn |
subscription.ended | subscription, change | Cancelled, expired, or replaced |
stock.low | stock | A sale crossed the low-stock threshold or reached zero |
- order: id, number, status, placement time, currency, precision, totals, email and customer, addresses, shipping and payment method, locale, subscription id, and lines with product, variation, title, SKU, quantity, amounts, tax, and options.
- subscription: id, number, status, customer, product and variation, quantity, renewal amount, interval, length, periods paid, period end, next payment, cancellation times, and the first order id.
changehas a type, a message, and a nullable order id. - refund: id, amount, status, reason, time, financial split, refunded lines and restock, and whether it was external. shipment: id, carrier and tracking, time, lines, and
complete. - stock: inventory, product, content, and variation ids, SKU, available, reserved, and committed counts, the threshold, and
level(loworout).
Listen from a plugin
import type { ShopEventMap } from "justflows.shop/contracts";
ctx.hooks.action("justflows.shop.order.paid", async (event) => {
// event.order.totalMinor and event.order.lines are typed by Shop's contracts.
await consumeOnce(event.eventKey, event.order);
});Send to another system
Add an HTTPS endpoint under core Admin → Webhooks and select the Shop events. Core queues each delivery durably, retries with exponential backoff for up to an hour (five attempts), and keeps a delivery log where you can retry failures. The body is { id, event, createdAt, data } with the Shop payload in data.
| Header | Value |
|---|---|
x-justflows-timestamp | Unix seconds. |
x-justflows-signature | sha256=<hex>: HMAC-SHA256 of timestamp + "." + raw body with the endpoint's secret. |
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyShopWebhook(rawBody, headers, secret) {
const timestamp = headers["x-justflows-timestamp"];
const signature = headers["x-justflows-signature"] ?? "";
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// After verifying: deduplicate on data.eventKey, store the event, then answer 2xx.Products are ordinary content, so core content webhooks already cover product changes: act on events where data.type is product.