Webhook Events Reference
Complete catalog of LostChurn webhook event types with payload schemas, field descriptions, and example JSON payloads.
This page is a comprehensive reference for every webhook event LostChurn emits. For setup instructions, retry policy, and endpoint management, see the Webhooks setup guide.
All events share the same envelope structure. The data object is event-specific and described for each event below.
Payload Envelope
Every webhook delivery wraps the event-specific payload in a common envelope:
{
"id": "evt_abc123def456",
"type": "recovery.succeeded",
"created_at": "2026-03-08T14:30:00Z",
"data": { ... }
}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID — use this for idempotent processing |
type | string | Event type string (e.g., recovery.succeeded) |
created_at | string | ISO 8601 UTC timestamp of when the event was generated |
data | object | Event-specific payload; schema documented per event below |
HMAC Signature Verification
Every delivery includes three headers for authenticity verification:
| Header | Description |
|---|---|
X-LostChurn-Signature | HMAC-SHA256 hex digest of {timestamp}.{raw_body} |
X-LostChurn-Timestamp | Unix timestamp of the delivery attempt |
X-LostChurn-Event-Id | Unique event ID (mirrors id in the payload) |
Verification steps:
- Read
X-LostChurn-TimestampandX-LostChurn-Signaturefrom the request headers. - Build the signed string:
{timestamp}.{raw_request_body}(use the raw bytes, not a parsed object). - Compute HMAC-SHA256 with your endpoint's signing secret.
- Compare using a constant-time function — reject if they differ.
- Reject the request if the timestamp is more than 5 minutes old (prevents replay attacks).
import crypto from "crypto";
export function verifyLostChurnWebhook(
headers: Record<string, string>,
rawBody: string,
signingSecret: string
): boolean {
const signature = headers["x-lostchurn-signature"];
const timestamp = headers["x-lostchurn-timestamp"];
if (!signature || !timestamp) return false;
// Replay protection: 5-minute window
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
throw new Error("Webhook timestamp outside tolerance window");
}
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}import hmac
import hashlib
import time
import json
def verify_lostchurn_webhook(headers, body, signing_secret):
signature = headers["X-LostChurn-Signature"]
timestamp = headers["X-LostChurn-Timestamp"]
now = int(time.time())
if abs(now - int(timestamp)) > 300:
raise ValueError("Webhook timestamp outside tolerance window")
payload = f"{timestamp}.{body}".encode("utf-8")
expected = hmac.new(
signing_secret.encode("utf-8"),
payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("Invalid webhook signature")
return json.loads(body)Payment Events
payment.failed
A new payment failure was recorded in LostChurn, typically triggered by an inbound PSP webhook.
{
"id": "evt_pf_01hx4y",
"type": "payment.failed",
"created_at": "2026-03-08T10:00:00Z",
"data": {
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"amount": 4999,
"currency": "usd",
"psp": "stripe",
"psp_payment_id": "ch_3OkZXY2eZvKYlo2C1234567",
"decline_code": "insufficient_funds",
"decline_category": "soft_retry",
"failed_at": "2026-03-08T09:59:50Z"
}
}| Field | Type | Description |
|---|---|---|
payment_id | string | LostChurn payment identifier |
customer_id | string | LostChurn customer identifier |
merchant_id | string | Merchant the payment belongs to |
amount | integer | Amount in smallest currency unit (e.g., cents) |
currency | string | ISO 4217 currency code (lowercase) |
psp | string | Payment service provider slug (e.g., stripe, adyen) |
psp_payment_id | string | Original payment ID from the PSP |
decline_code | string | Normalized decline code |
decline_category | string | Decline category: soft_retry, hard, fraud, etc. |
failed_at | string | ISO 8601 UTC timestamp from the PSP |
payment.recovered
A previously failed payment was successfully recovered via a retry or out-of-band payment.
{
"id": "evt_pr_02hy5z",
"type": "payment.recovered",
"created_at": "2026-03-09T11:15:00Z",
"data": {
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"amount": 4999,
"currency": "usd",
"psp": "stripe",
"psp_payment_id": "ch_3OkZXY2eZvKYlo2C1234567",
"recovered_at": "2026-03-09T11:14:55Z",
"retry_count": 2,
"recovery_method": "silent_retry"
}
}| Field | Type | Description |
|---|---|---|
retry_count | integer | Number of retry attempts before success |
recovered_at | string | ISO 8601 UTC timestamp of the successful charge |
recovery_method | string | silent_retry, dunning, payment_method_update, or manual |
payment.terminal
All recovery attempts exhausted; the payment is marked terminal and will not be retried.
{
"id": "evt_pt_03iz6a",
"type": "payment.terminal",
"created_at": "2026-03-12T08:00:00Z",
"data": {
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"amount": 4999,
"currency": "usd",
"psp": "stripe",
"decline_code": "do_not_honor",
"decline_category": "hard",
"terminal_reason": "hard_decline",
"terminal_at": "2026-03-12T07:59:30Z"
}
}| Field | Type | Description |
|---|---|---|
terminal_reason | string | hard_decline, max_retries_reached, fraud_flagged, or manual |
terminal_at | string | ISO 8601 UTC timestamp when terminal status was set |
Recovery Events
recovery.started
A new recovery process has been created for a failed payment.
{
"id": "evt_rs_04ja7b",
"type": "recovery.started",
"created_at": "2026-03-08T10:01:00Z",
"data": {
"recovery_id": "rec_001aaa",
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"decline_category": "soft_retry",
"phase": "silent",
"scheduled_retries": 3,
"started_at": "2026-03-08T10:01:00Z"
}
}| Field | Type | Description |
|---|---|---|
recovery_id | string | LostChurn recovery process identifier |
phase | string | Initial phase: silent or active |
scheduled_retries | integer | Number of silent retries scheduled |
started_at | string | ISO 8601 UTC timestamp |
recovery.retry_attempted
A silent retry was submitted to the PSP.
{
"id": "evt_rra_05kb8c",
"type": "recovery.retry_attempted",
"created_at": "2026-03-09T06:00:00Z",
"data": {
"recovery_id": "rec_001aaa",
"retry_attempt_id": "rta_111bbb",
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"attempt_number": 1,
"psp": "stripe",
"status": "pending",
"attempted_at": "2026-03-09T06:00:00Z"
}
}| Field | Type | Description |
|---|---|---|
retry_attempt_id | string | Unique ID for this specific retry attempt |
attempt_number | integer | Sequential attempt index (1-based) |
status | string | pending, succeeded, or failed |
recovery.succeeded
The recovery process ended successfully — a retry or payment method update charged the customer.
{
"id": "evt_rsc_06lc9d",
"type": "recovery.succeeded",
"created_at": "2026-03-09T11:15:00Z",
"data": {
"recovery_id": "rec_001aaa",
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"amount": 4999,
"currency": "usd",
"decline_code": "insufficient_funds",
"decline_category": "soft_retry",
"retry_count": 2,
"recovered_at": "2026-03-09T11:14:55Z",
"psp": "stripe"
}
}recovery.failed
All recovery attempts have been exhausted without success. The payment is now terminal.
{
"id": "evt_rf_07md0e",
"type": "recovery.failed",
"created_at": "2026-03-12T08:00:00Z",
"data": {
"recovery_id": "rec_001aaa",
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"retry_count": 3,
"final_decline_code": "do_not_honor",
"final_decline_category": "hard",
"failed_at": "2026-03-12T07:59:30Z"
}
}recovery.escalated
Silent retries were exhausted and the recovery has moved into the active dunning phase (email/SMS campaigns).
{
"id": "evt_re_08ne1f",
"type": "recovery.escalated",
"created_at": "2026-03-11T06:00:00Z",
"data": {
"recovery_id": "rec_001aaa",
"payment_id": "pay_abc123",
"customer_id": "cus_def456",
"previous_phase": "silent",
"new_phase": "active",
"silent_retries_attempted": 3,
"escalated_at": "2026-03-11T06:00:00Z"
}
}Campaign Events
campaign.enrolled
A customer was enrolled into a dunning campaign.
{
"id": "evt_ce_09of2g",
"type": "campaign.enrolled",
"created_at": "2026-03-11T06:05:00Z",
"data": {
"campaign_id": "cmp_aaa111",
"campaign_name": "3-Touch Soft Decline Recovery",
"customer_id": "cus_def456",
"payment_id": "pay_abc123",
"recovery_id": "rec_001aaa",
"enrolled_at": "2026-03-11T06:05:00Z"
}
}campaign.sent
A campaign message (email, SMS, push, or WhatsApp) was submitted to the delivery provider.
{
"id": "evt_cs_10pg3h",
"type": "campaign.sent",
"created_at": "2026-03-11T08:00:00Z",
"data": {
"campaign_id": "cmp_aaa111",
"campaign_name": "3-Touch Soft Decline Recovery",
"step_index": 0,
"channel": "email",
"customer_id": "cus_def456",
"customer_email": "jane@example.com",
"message_id": "msg_xyz789",
"sent_at": "2026-03-11T08:00:00Z"
}
}| Field | Type | Description |
|---|---|---|
campaign_id | string | Campaign identifier |
campaign_name | string | Campaign display name |
step_index | integer | Zero-based step index in the campaign sequence |
channel | string | email, sms, push, or whatsapp |
customer_id | string | LostChurn customer identifier |
customer_email | string | Recipient email address (email channel only) |
message_id | string | Unique message delivery identifier |
campaign.opened
A campaign email was opened by the recipient.
{
"id": "evt_co_11qh4i",
"type": "campaign.opened",
"created_at": "2026-03-11T09:30:00Z",
"data": {
"campaign_id": "cmp_aaa111",
"step_index": 0,
"channel": "email",
"customer_id": "cus_def456",
"message_id": "msg_xyz789",
"opened_at": "2026-03-11T09:30:00Z"
}
}campaign.clicked
A recipient clicked a link inside a campaign message.
{
"id": "evt_ck_12ri5j",
"type": "campaign.clicked",
"created_at": "2026-03-11T09:32:00Z",
"data": {
"campaign_id": "cmp_aaa111",
"step_index": 0,
"channel": "email",
"customer_id": "cus_def456",
"message_id": "msg_xyz789",
"link_url": "https://pay.example.com/update?token=abc",
"clicked_at": "2026-03-11T09:32:00Z"
}
}campaign.bounced
A campaign message was rejected or could not be delivered.
{
"id": "evt_cb_13sj6k",
"type": "campaign.bounced",
"created_at": "2026-03-11T08:01:00Z",
"data": {
"campaign_id": "cmp_aaa111",
"step_index": 0,
"channel": "email",
"customer_id": "cus_def456",
"customer_email": "jane@example.com",
"message_id": "msg_xyz789",
"bounce_type": "hard",
"bounce_reason": "invalid_address",
"bounced_at": "2026-03-11T08:01:00Z"
}
}| Field | Type | Description |
|---|---|---|
bounce_type | string | hard (permanent) or soft (temporary) |
bounce_reason | string | invalid_address, mailbox_full, domain_not_found, etc. |
campaign.unsubscribed
A recipient unsubscribed from dunning communications.
{
"id": "evt_cu_14tk7l",
"type": "campaign.unsubscribed",
"created_at": "2026-03-11T10:00:00Z",
"data": {
"campaign_id": "cmp_aaa111",
"customer_id": "cus_def456",
"customer_email": "jane@example.com",
"message_id": "msg_xyz789",
"unsubscribed_at": "2026-03-11T10:00:00Z"
}
}Customer Events
customer.payment_method_updated
A customer updated their payment method via a dunning link or the payment update widget.
{
"id": "evt_cpu_15ul8m",
"type": "customer.payment_method_updated",
"created_at": "2026-03-11T12:00:00Z",
"data": {
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"payment_id": "pay_abc123",
"recovery_id": "rec_001aaa",
"new_card_last4": "4242",
"new_card_brand": "visa",
"updated_at": "2026-03-11T12:00:00Z"
}
}customer.subscription_canceled
A customer completed the cancel flow and confirmed their cancellation.
{
"id": "evt_csc_16vm9n",
"type": "customer.subscription_canceled",
"created_at": "2026-03-10T15:30:00Z",
"data": {
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"cancel_session_id": "cs_zzz999",
"cancellation_reason": "too_expensive",
"canceled_at": "2026-03-10T15:30:00Z"
}
}| Field | Type | Description |
|---|---|---|
cancel_session_id | string | Cancel flow session identifier |
cancellation_reason | string | Customer-selected reason code |
customer.retained
A customer accepted a retention offer during the cancel flow and did not cancel.
{
"id": "evt_cr_17wn0o",
"type": "customer.retained",
"created_at": "2026-03-10T15:32:00Z",
"data": {
"customer_id": "cus_def456",
"merchant_id": "mer_789xyz",
"cancel_session_id": "cs_zzz999",
"offer_type": "discount",
"offer_value": "20_percent_off_3_months",
"retained_at": "2026-03-10T15:32:00Z"
}
}| Field | Type | Description |
|---|---|---|
offer_type | string | discount, pause, downgrade, or free_month |
offer_value | string | Human-readable description of the offer applied |
Quick Reference
| Event | Category | Trigger |
|---|---|---|
payment.failed | Payment | PSP webhook received; decline recorded |
payment.recovered | Payment | Successful retry or out-of-band payment |
payment.terminal | Payment | All recovery exhausted; no further retries |
recovery.started | Recovery | Failed payment classified; recovery opened |
recovery.retry_attempted | Recovery | Silent retry submitted to PSP |
recovery.succeeded | Recovery | Recovery process ended successfully |
recovery.failed | Recovery | All retries exhausted; payment terminal |
recovery.escalated | Recovery | Silent phase ended; active dunning started |
campaign.enrolled | Campaign | Customer added to dunning campaign |
campaign.sent | Campaign | Message submitted to delivery provider |
campaign.opened | Campaign | Email opened by recipient |
campaign.clicked | Campaign | Link in message clicked by recipient |
campaign.bounced | Campaign | Message delivery failed |
campaign.unsubscribed | Campaign | Customer opted out of dunning |
customer.payment_method_updated | Customer | Card updated via dunning link or widget |
customer.subscription_canceled | Customer | Cancel flow completed with cancellation |
customer.retained | Customer | Cancel flow completed with offer accepted |
Related
- Webhooks Setup Guide — configure endpoints, view delivery logs, manage signing secrets
- Authentication — webhook signing secrets and key management
- Payments API — query and retry payments programmatically
- Cancel Sessions API — retrieve cancel session outcomes referenced in customer events