Webhooks
Receive real-time notifications when events occur in a Cartly store.
Available Events
Subscribe to these webhook event types. Send test also delivers a webhook.test event, which you cannot subscribe to.
| Name | Type | Required | Description |
|---|---|---|---|
| order.created | event | No | Fired when a new order is placed |
| order.fulfilled | event | No | Fired when an order is marked as fulfilled |
| order.cancelled | event | No | Fired when an order is cancelled |
| order.paid | event | No | Fired when payment is confirmed |
| product.created | event | No | Fired when a product is created |
| product.updated | event | No | Fired when a product is modified |
| product.deleted | event | No | Fired when a product is deleted |
| customer.created | event | No | Fired when a customer account is created through the Headless Storefront API or the Apps API |
| app/uninstalled | event | No | Fired when your app is uninstalled; it is the last event the app receives |
Delivery Headers
Every delivery carries the same headers: the first attempt, a retry from the failed-delivery queue and a Send test.
X-Cartly-Signature-V2:t=<unix seconds>,v1=<hex>, the HMAC-SHA256 oft + "." + raw bodywith your secret. Verify this one; it binds a timestamp, so a replayed request can be rejectedX-Cartly-Timestamp: the sametX-Cartly-Hmac-Sha256(aliasX-Cartly-Signature):sha256=<hex>, the HMAC of the raw body only. Deprecated: it has no replay protectionX-Cartly-Event-Id: the id of the event, the same on every delivery of that event. Dedupe on itX-Cartly-Webhook-Id: the subscription the delivery is forX-Cartly-Shop-Domain: the store address,<slug>.mycartly.pro
The signature headers are sent when the subscription has a secret. Content-Type: application/json and User-Agent: Cartly-Webhook/1.0 are always sent.
Verify Webhook Signature (X-Cartly-Signature-V2)
const crypto = require('crypto');
// header = X-Cartly-Signature-V2: "t=<unix seconds>,v1=<hex>"; rawBody = the request body bytes as received
function verifyWebhook(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
const got = Buffer.from(parts.v1 || '', 'hex');
const want = Buffer.from(expected, 'hex');
return got.length === want.length && crypto.timingSafeEqual(got, want);
}Delivery and Retries
Cartly sends each event once to each subscribed endpoint and waits up to 10 seconds for a 2xx response. Redirects are not followed: a 3xx response is a failed delivery, so register the final URL. Any other status, a timeout or a refused connection is a failed delivery too. Send test and a retry from the failed-delivery queue wait up to 30 seconds, so a slow endpoint can pass Send test and still time out on live deliveries.
A failed delivery is kept in the store's failed-delivery queue (the dead-letter queue, under Settings → Webhooks in the admin), where it can be retried or discarded. A retry carries the original X-Cartly-Event-Id.
After 5 failed deliveries in a row spanning at least an hour the subscription is automatically disabled. A short outage of your endpoint does not switch it off, and a delivery that goes through resets the count. A successful Send test or retry re-enables a disabled subscription. Events that happen while a subscription is disabled are not queued for it.
Expected Response
Return a 2xx within 10 seconds and process the event asynchronously. The same event can arrive twice (a retry after a timeout): make your handler idempotent on X-Cartly-Event-Id.
Managing Webhooks in the Admin
Merchants manage subscriptions under Settings → Webhooks (/admin/webhooks):
- Status: Active, Failing while deliveries are failing, or Disabled after the auto-disable rule above. A failing or disabled subscription shows its last error.
- Send test sends one signed
webhook.testevent to the endpoint, without retries. If the endpoint answers2xx, the subscription is active again. - Failed deliveries lists what your endpoint did not accept, with Retry, Discard and Retry All. The list is paginated.
When an App Is Uninstalled
When a store uninstalls an app, the app's access tokens are revoked and its webhook subscriptions stop receiving events right away, including REST hooks created by integrations such as Zapier and Make. Their queued failed deliveries are discarded. The one exception is a subscription to app/uninstalled: it still receives that single event and is then removed. The event data identifies the store and the listing (shop_id, listing_id, listing_slug).