Receive lifecycle events at a webhook
Each recipe posts one signed JSON event to your endpoint. Your code checks the signature and acts on the fields.
All lifecycle guides · Checked against AXO’s adapters in October 2026.
Before you start
Connect Webhook in AXO first: see Connect Webhook. The integration must be active. In the dashboard, the Install button is off for read-only members.
When AXO sends
A recipe is installed as an ordinary AXO trigger, turned off. Once you turn it on, AXO watches for the moment, waits where the recipe waits, skips people who converted or are in their cooldown, and sends the event. Every lifecycle event needs the person’s marketing consent. A holdout group gets nothing so that the trigger’s lift can be measured. Event names:
- Abandoned cart:
axo.lifecycle.abandoned_cart - Abandoned browse:
axo.lifecycle.abandoned_browse - Abandoned checkout:
axo.lifecycle.abandoned_checkout - High-intent return:
axo.lifecycle.high_intent_return - Lapsed visitor:
axo.lifecycle.lapsed_visitor
See Create a trigger for cooldowns, holdout and fire history.
Build the receiving side
AXO posts the canonical lifecycle event as JSON to the webhook integration’s URL (custom integrations use the same path). The request has three headers:
X-VibeTag-Signature: the hex HMAC-SHA256 of the raw body, keyed by the integration’s secret. Set a secret: without one, AXO signs with your public site id, which anyone with your install snippet can reproduce.X-VibeTag-Trigger: the trigger id.X-VibeTag-Identifier-Type: the identifier type the recipe sends to, such asemail.
- Build an endpoint at a public HTTPS address. AXO refuses private and loopback addresses.
- Verify the signature against the raw body before reading it, as in the example below.
- Drop repeats by
delivery_id, which stays the same when one moment is sent again. Test sends use ids that start withlcd_test_. - Answer with a 2xx status within 8 seconds. A 408, 429 or 503 answer, or a connection that never opens (refused, DNS or TLS failure, connect timeout), is retried with the same body and
delivery_id: up to 5 attempts in all, spaced about 2 minutes, 15 minutes, 1 hour and 4 hours apart, for as long as the recipe’s window setting still allows the send. The fire history showsretryinguntil one succeeds or the attempts run out. Any other status, or a timeout after the connection opened, is recorded as an error and not retried, since your endpoint may already have acted on it. - In AXO, install the recipe with the webhook as a destination, send a test from the trigger’s Test fire button, then turn the trigger on (Turn on right after install, or Resume on the trigger).
import crypto from "node:crypto";
// secret: the value set on the webhook integration in AXO.
// Hash the raw request body exactly as received.
function verify(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader || "", "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// In the handler: reject a bad signature, skip a delivery_id you have
// already seen, then answer 2xx within 8 seconds.Fields
Top-level fields of the JSON body.
| Field | Type | Meaning |
|---|---|---|
schema_version | number | Version of this event shape. Currently 1. |
event_name | text | axo.lifecycle.<recipe_id>, for example axo.lifecycle.abandoned_cart. |
recipe_id | text | The recipe that fired. |
recipe_version | number | The recipe version the trigger was installed with. |
occurred_at | text | When AXO fired the event (ISO 8601, UTC). |
fire_id | text | AXO’s id for this send attempt. |
delivery_id | text | The same on every attempt for one moment and one destination. Use it to drop duplicates. Test sends start with lcd_test_. |
trigger_id | text | The AXO trigger the recipe was installed as. |
site | object | The AXO site: { id, domain }. |
profile | object | The person: { id, identifiers: { email, email_sha256, phone, external_id, anonymous_id } }. Identifiers the person does not have are left out. |
consent_basis | text | Always marketing. AXO sends lifecycle events only for people with marketing consent. |
items | list | Line items or viewed products. See the item fields below. |
cart_value | number | Cart value as a number, or null when unknown. |
currency | text | ISO 4217 code such as USD, or null. |
resume_url | text | A link back to the cart or page. Real sends carry a vt_token that signs the person back in; test sends carry the plain link. |
source | object | The moment that started the recipe: { event_type, page_url, occurred_at }. |
settings | object | The settings the recipe was installed with. |
Item fields
Each entry in items has these fields.
| Field | Type | Meaning |
|---|---|---|
product_id | text | Product id (the numeric Shopify id for Shopify catalogs). |
sku | text | SKU. |
name | text | Product name. |
url | text | Product page URL. |
image_url | text | Product image URL. |
price | number | Price as a number. |
previous_price | number | Always null for now. It will hold the price before a drop once the catalog keeps price history. |
currency | text | ISO 4217 code. |
availability | text | Availability text from the catalog, such as in_stock. |
quantity | number | Quantity in the cart. Null for viewed products. |