Custom Events
Define your own event types — payload shape, identifier mapping, and how they flow into LiveRecover.
Every event you send to LiveRecover's Custom E-Com webhook is a custom event — a brand-defined event type with:
- A unique slug (the
typeyou'll send on the wire). - A JSON Schema describing the payload.
- An identifier mapping that tells LiveRecover which fields in your payload identify the customer.
You declare the shape of your data; LiveRecover validates incoming payloads against your schema and records each event. There is no closed list of "supported" topics — model whatever event your application produces (abandoned checkout, loyalty tier change, post- purchase survey, wishlist add) as a custom event slug.
A registered custom event can trigger campaign flows. Once you build and activate a flow on your event, accepted events become flow candidates asynchronously — a poll picks them up within a few minutes and runs your flow's messaging. See Triggering flows below.
How it works
- Register a definition in
Settings → Providers → Custom E-Com (click Add event). Pick a
typeslug, paste a JSON Schema, and set the identifier mapping. You can also do this from your own code — see Managing your integration in the Brand Data API. - Send events with
x-vyg-topic: <your-slug>to the webhook URL. - LiveRecover validates the payload against your schema, resolves the customer using your identifier mapping, and records the event.
- Build a flow on your event in the campaign builder. Once that flow is active, recorded events trigger it on the next poll cycle — see Triggering flows.
Slug rules
The type must match ^[a-z0-9_]{1,64}$:
- Lowercase ASCII letters, digits, and underscores.
- 1 to 64 characters.
- No slashes, hyphens, dots, or unicode.
Examples: loyalty_tier_changed, survey_completed, wishlist_add.
Payload schema
A definition's payload_schema is a standard
JSON Schema (draft-07) document. It
is stored verbatim and compiled with ajv at
request time. The validator runs in non-strict mode with allErrors: true, so all violations are reported in a single response.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["customer_id", "tier"],
"properties": {
"customer_id": { "type": "string" },
"email": { "type": "string", "format": "email" },
"tier": { "type": "string", "enum": ["bronze", "silver", "gold"] },
"changed_at": { "type": "string", "format": "date-time" }
}
}additionalProperties is allowed by default — extra fields are stored
on the event but ignored by the matching logic. Set
"additionalProperties": false if you want strict rejection.
When you update a definition's schema, the stored schema_version is
bumped automatically; in-flight ajv validators are recompiled on the
next request.
Identifier mapping
LiveRecover needs to identify the customer behind every event so it can
correlate with conversations, contacts, and recovery flows. The
identifier_mappings field tells us where to look in your payload.
type CustomEventIdentifierMappings = {
external_id_path: string[]; // required, ≥1
email_path?: string[];
phone_path?: string[];
event_id_path?: string[];
name_path?: string[];
first_name_path?: string[];
last_name_path?: string[];
received_at_path?: string[];
};Each value is an ordered list of dot-paths into the payload. The
resolver tries each path in order and takes the first non-empty value
(null, undefined, and the empty string all skip).
Required fields
external_id_path— at least one path that always resolves. The resolved value is your stable customer identity on this integration.- At least one of
email_path/phone_path— without an email or phone, LiveRecover has no channel to reach the shopper, and the event will be rejected with422 missing_contact_channel.
Example
For a payload like:
{
"customer_id": "cust_001",
"customer": { "email": "shopper@example.com" },
"contact": { "phone": "+15551234567" },
"meta": { "source_event": "evt_abc" }
}A reasonable mapping is:
{
"external_id_path": ["customer_id"],
"email_path": ["customer.email", "email"],
"phone_path": ["contact.phone", "customer.phone"],
"event_id_path": ["meta.source_event"]
}The OR fallback is the point: brands evolve their schemas, and
["customer.email", "email"] lets old and new payload shapes resolve
without a coordinated migration.
Wire format
Identical to the built-in topics — see
Webhook Endpoint Reference for headers
and HMAC requirements. The only difference is that type (and the
matching x-vyg-topic header) is your registered slug.
type CustomWebhookBody = {
type: string; // your registered slug, e.g. "loyalty_tier_changed"
event_id: string; // matches the x-vyg-event-id header
data: Record<string, unknown>; // validated against your payload_schema
};data must be a JSON object. Arrays and scalars are rejected with
400 invalid_payload.
Sample request
Assuming a definition registered with slug loyalty_tier_changed and
the schema / mapping above:
SECRET="your-webhook-secret"
URL="https://<your-vyg-webhook-host>/custom/V1StGXR8_Z5jdHi6B-myT"
BODY='{"type":"loyalty_tier_changed","event_id":"evt_loyalty_1","data":{"customer_id":"cust_001","customer":{"email":"shopper@example.com"},"contact":{"phone":"+15551234567"},"tier":"gold","changed_at":"2026-05-08T10:15:00Z"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -sS -X POST "$URL" \
-H "content-type: application/json" \
-H "x-vyg-topic: loyalty_tier_changed" \
-H "x-vyg-event-id: evt_loyalty_1" \
-H "x-vyg-signature: $SIG" \
--data "$BODY"A successful response includes the recorded event id. flow_event_id is
always null in the response: the webhook records the event synchronously,
but flow triggering happens asynchronously afterward, so no flow id
exists yet at the moment the webhook replies. This response contract is
unchanged — see Triggering flows for where flow
activity shows up instead.
{
"ok": true,
"custom_event_id": "8e3b0f5a-...-...",
"flow_event_id": null
}A duplicate (same event_id within 24h) short-circuits with:
{ "deduplicated": true }Error codes
In addition to the standard webhook statuses, custom events surface a few event-specific reasons:
| Status | Reason | Cause | Should you retry? |
|---|---|---|---|
400 | invalid_payload | Payload failed JSON Schema validation, or data is not an object. details lists ajv error objects. | No — fix the payload and re-send with a new event_id. |
404 | unknown_event_type | No enabled definition matches the (brandIntegrationId, type) lookup. | No — register or re-enable the definition, then re-send. |
422 | missing_external_id | The external_id_path list resolved to nothing for this payload. | No — fix the payload or extend external_id_path. |
422 | missing_contact_channel | Neither email_path nor phone_path resolved a value. | No — include an email or phone, or extend the mapping. |
404 unknown_event_type does not mark the event as seen in the
idempotency cache. You can register the definition and replay the same
event_id afterwards.
401 (HMAC) and 500 (transient) behave as documented in
Errors & Retry Semantics.
What happens after a successful 200
- The event payload is recorded against your integration.
- Contact information (email, phone, name) from the resolved identifiers is upserted, so subsequent messaging targets the right shopper.
Even when a contact cannot be resolved from a valid external_id, the
event is still recorded and available for later backfill.
Triggering flows
A recorded custom event can trigger a campaign flow. The webhook does not dispatch the flow inline; flow triggering runs on a separate poll cycle after the event is recorded.
How triggering works
- Build a flow on your event. In the campaign builder, the trigger node lists your registered custom events alongside the predefined ones. Selecting your event binds the flow to its slug.
- Activate the flow. Only active, non-paused flows are eligible. A recorded event triggers a flow only if a matching flow is active at poll time.
- The poll picks it up. A background poll runs on a fixed schedule (about every five minutes) and selects recently recorded events that match an active flow on the same integration. It looks back over a bounded recency window, so build and activate your flow before — or shortly after — events start arriving.
- The flow runs. For each matched event, LiveRecover dispatches the flow and runs its nodes (filters, messages, delays) using your event's payload. Your registered schema fields are available throughout — see Schema fields in the campaign builder.
Because triggering is asynchronous, expect a short delay (typically under
five minutes) between a 200 webhook response and any message your flow
sends. The webhook response itself is unchanged: flow_event_id is always
null because no flow has run yet when the webhook replies.
Gating
Two conditions must hold for a custom event to trigger a flow:
- The brand is enrolled in the custom integration (which is what lets you send events at all).
- A matching flow is active for the event slug on the same integration. No active flow means no dispatch — the event is still recorded.
Triggering availability is enabled by your LiveRecover account contact; it is not self-serve.
Once-per-event delivery
Each recorded event triggers a matching flow at most once. The trigger
layer keys on the event's external id together with the brand and event
type, so a replayed delivery (same event_id) does not produce a second
flow run. Re-sending the same event is safe.
Where flow activity is visible
Because the webhook response carries no flow id, look for flow activity in the LiveRecover dashboard rather than in the webhook response:
- The recorded event appears under Settings → Providers → Custom E-Com: open the event definition and check its Events tab, which lists recent events (most recent first).
- Flow runs and the messages they send appear in the campaign's activity in the LiveRecover dashboard, the same place every other campaign flow reports.
Schema fields in the campaign builder
The JSON Schema you register for an event is not just for validation — it also drives what you can target and personalize in the campaign builder:
- Trigger selection. Your registered events appear by name in the trigger node's event menu.
- Node filters. Each node's filter picker offers your event's payload fields (all leaf and array fields), so you can branch on your own data — alongside the standard contact and brand facts.
- Message variables. SMS and agent message composers offer your
event's payload fields as
event.*variables, alongside the common brand, contact, and generated variables (including discount codes). This applies to AI-assisted message writing as well.
So the same schema that gates which payloads are accepted at the webhook also defines the fields you can filter on and reference in messages.