LiveRecoverVYG Developer Docs
Custom IntegrationCustom EventsRecipes

Abandoned Checkout

End-to-end recipe for modelling abandoned checkouts as a custom event — schema, mapping, and payload.

A worked example: model "shopper started checkout but didn't complete it" as a custom event called checkout_abandoned, register it, send a signed payload that LiveRecover validates and records, and trigger a recovery flow off it.

This recipe covers registering the event and getting a payload accepted, then building a flow that triggers on it. Once the flow is active, recorded events trigger it asynchronously on the next poll cycle — see Custom Events → Triggering flows.

1. Define the event

Register a definition under Settings → Providers → Custom E-Com (click Add event) with this payload schema:

{
	"$schema": "http://json-schema.org/draft-07/schema#",
	"type": "object",
	"required": ["checkout_id", "customer_email", "cart_total", "currency", "abandoned_at"],
	"properties": {
		"checkout_id": { "type": "string" },
		"customer_email": { "type": "string", "format": "email" },
		"customer_phone": { "type": "string" },
		"customer_first_name": { "type": "string" },
		"customer_last_name": { "type": "string" },
		"cart_total": { "type": "number" },
		"currency": { "type": "string" },
		"items": {
			"type": "array",
			"items": {
				"type": "object",
				"properties": {
					"sku": { "type": "string" },
					"title": { "type": "string" },
					"quantity": { "type": "integer" },
					"price": { "type": "number" }
				}
			}
		},
		"abandoned_at": { "type": "string", "format": "date-time" }
	}
}

2. Map identifiers

The identifier mapping tells LiveRecover which fields identify the shopper. For this schema:

{
	"external_id_path": ["checkout_id"],
	"email_path": ["customer_email"],
	"phone_path": ["customer_phone"],
	"first_name_path": ["customer_first_name"],
	"last_name_path": ["customer_last_name"],
	"received_at_path": ["abandoned_at"]
}

external_id_path resolves a stable id for the abandoned checkout. The first email or phone we can resolve becomes the messaging channel; both is best.

3. Send the event

Sign the body with the brand's webhook secret (see HMAC Verification) and POST to the Custom E-Com endpoint:

SECRET='whsec_demo_secret'
URL='https://<your-vyg-webhook-host>/custom/<webhookId>'
BODY='{"type":"checkout_abandoned","event_id":"evt_chk_001","data":{"checkout_id":"ck_9f3a","customer_email":"jane@example.com","customer_phone":"+15551234567","customer_first_name":"Jane","customer_last_name":"Doe","cart_total":89.50,"currency":"USD","items":[{"sku":"COF-001","title":"Monthly Coffee","quantity":1,"price":29.99}],"abandoned_at":"2026-05-09T14:32:11Z"}}'

SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
# expected: 259cf6883b6322233d897ce4f1279a12807b3a57e4ed64de0eb6ae9b49015a60

curl -sS -X POST "$URL" \
  -H 'content-type: application/json' \
  -H 'x-vyg-topic: checkout_abandoned' \
  -H 'x-vyg-event-id: evt_chk_001' \
  -H "x-vyg-signature: $SIG" \
  --data "$BODY"

A successful response (flow_event_id is always null — the event is recorded synchronously, while flow triggering runs asynchronously afterward):

{
	"ok": true,
	"custom_event_id": "8e3b0f5a-…-…",
	"flow_event_id": null
}

4. Verify end-to-end

After sending the request:

  1. The event appears under Settings → Providers → Custom E-Com: open the checkout_abandoned definition and check its Events tab, which lists recent events (most recent first).
  2. The contact (Jane Doe) is upserted with the email and phone you sent, so messaging targets the right shopper.
  3. If you have built and activated a flow on checkout_abandoned, the recorded event triggers it asynchronously on the next poll cycle (typically within a few minutes) — see Custom Events → Triggering flows. Flow runs appear in the campaign's activity in the LiveRecover dashboard.

If you see 404 unknown_event_type, your type slug doesn't match a registered definition. If you see 422 missing_external_id or missing_contact_channel, recheck the identifier mapping against your real payload.

On this page