LostChurn Docs
API Reference

Webhooks

Outbound webhook events from LostChurn — event types, payload format, retry policy, and signature verification.

LostChurn sends webhook events to your application when key recovery and campaign actions occur. Use webhooks to keep your systems in sync with recovery status, update your UI, or trigger custom workflows.

Webhook Endpoints

Configure webhook endpoints in the LostChurn dashboard under Settings > Webhooks. Each endpoint specifies:

  • URL: The HTTPS endpoint on your server that receives events
  • Events: Which event types to send (subscribe to all or select specific types)
  • Signing secret: Used to verify payload authenticity

You can create multiple endpoints to route different event types to different services.

Event Types

Recovery Events

EventDescriptionTrigger
recovery.startedA new recovery process has begunFailed payment received and classified
recovery.retry_attemptedA silent retry was submittedRetry executed via PSP
recovery.succeededPayment was successfully recoveredRetry or payment method update succeeded
recovery.failedRecovery exhausted all attemptsAll retries failed, dunning complete
recovery.escalatedRecovery moved from silent to active phaseSilent retries exhausted, dunning triggered

Campaign Events

EventDescriptionTrigger
campaign.sentA campaign message was sentEmail, SMS, push, or WhatsApp delivered to PSP
campaign.openedA campaign email was openedRecipient opened the email
campaign.clickedA campaign link was clickedRecipient clicked a link in the message
campaign.bouncedA campaign message bouncedEmail address invalid or mailbox full
campaign.unsubscribedRecipient unsubscribed from dunningCustomer opted out

Customer Events

EventDescriptionTrigger
customer.payment_method_updatedCustomer updated their payment methodCard updated via dunning link or widget
customer.subscription_canceledCustomer canceled via cancel flowCancel flow widget completed
customer.retainedCustomer accepted a retention offerCancel flow offer accepted

Payment Events

EventDescriptionTrigger
payment.failedA new payment failure was recordedWebhook received from PSP
payment.recoveredA failed payment was recoveredSuccessful retry or manual payment
payment.terminalA payment was marked terminalTerminal decline code or manual action

Payload Format

All webhook payloads follow a consistent structure:

{
  "id": "evt_abc123def456",
  "type": "recovery.succeeded",
  "created_at": "2025-03-08T14:30:00Z",
  "data": {
    "recovery_id": "rec_789xyz",
    "payment_id": "pay_abc123",
    "customer_id": "cus_def456",
    "amount": 4999,
    "currency": "usd",
    "decline_code": "insufficient_funds",
    "decline_category": "soft_retry",
    "retry_count": 2,
    "recovered_at": "2025-03-08T14:30:00Z",
    "psp": "stripe"
  }
}

Common Fields

FieldTypeDescription
idstringUnique event ID
typestringEvent type (e.g., recovery.succeeded)
created_atstringISO 8601 timestamp in UTC
dataobjectEvent-specific payload (varies by type)

Recovery Event Data

FieldTypeDescription
recovery_idstringLostChurn recovery ID
payment_idstringOriginal failed payment ID
customer_idstringCustomer ID from your PSP
amountintegerPayment amount in smallest currency unit
currencystringThree-letter ISO currency code
decline_codestringRaw decline code from PSP
decline_categorystringLostChurn category: soft_retry, hard_customer, terminal, unknown
retry_countintegerNumber of retry attempts made
pspstringPayment processor name

Campaign Event Data

FieldTypeDescription
campaign_idstringCampaign ID
campaign_namestringCampaign display name
step_indexintegerStep number in the campaign sequence
channelstringDelivery channel: email, sms, push, whatsapp
customer_idstringCustomer ID
customer_emailstringCustomer email (for email events)
message_idstringUnique message ID

Signature Verification

Every webhook request includes a signature header that you should verify to ensure the payload was sent by LostChurn and has not been tampered with.

Headers

HeaderDescription
X-LostChurn-SignatureHMAC-SHA256 signature of the request body
X-LostChurn-TimestampUnix timestamp of when the event was sent
X-LostChurn-Event-IdUnique event ID (same as id in the payload)

Verification Process

  1. Read the X-LostChurn-Timestamp and X-LostChurn-Signature headers.
  2. Concatenate the timestamp and the raw request body with a . separator: {timestamp}.{body}.
  3. Compute an HMAC-SHA256 hash using your webhook signing secret.
  4. Compare the computed hash to the signature header using a constant-time comparison.
  5. Verify the timestamp is within 5 minutes of your server's current time.

Node.js Verification Example

import crypto from "node:crypto";

function verifyLostChurnWebhook(req, signingSecret) {
  const signature = req.headers["x-lostchurn-signature"];
  const timestamp = req.headers["x-lostchurn-timestamp"];
  const body = req.rawBody; // raw request body as string

  // Check timestamp tolerance (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error("Webhook timestamp outside tolerance window");
  }

  // Compute expected signature
  const payload = `${timestamp}.${body}`;
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(payload)
    .digest("hex");

  // Constant-time comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );

  if (!isValid) {
    throw new Error("Invalid webhook signature");
  }

  return JSON.parse(body);
}

Python Verification Example

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"]

    # Check timestamp tolerance (5 minutes)
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp outside tolerance window")

    # Compute expected signature
    payload = f"{timestamp}.{body}".encode("utf-8")
    expected = hmac.new(
        signing_secret.encode("utf-8"),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    if not hmac.compare_digest(expected, signature):
        raise ValueError("Invalid webhook signature")

    return json.loads(body)

Retry Policy

If your endpoint returns a non-2xx status code or does not respond within 30 seconds, LostChurn retries the delivery:

AttemptDelay After Previous
1st retry1 minute
2nd retry10 minutes
3rd retry1 hour

After 3 failed retries (4 total attempts), the event is marked as failed. Failed events can be viewed and manually retried from Settings > Webhooks > Failed Events in the dashboard.

Your endpoint should return a 200 status code as quickly as possible. Process the webhook payload asynchronously to avoid timeouts. If your endpoint consistently fails, LostChurn will disable the endpoint and notify you via email.

Inbound Webhook Deduplication

When LostChurn receives webhooks from your payment processor, duplicate events are automatically rejected at the edge. The deduplication layer uses a 24-hour sliding window keyed on the event ID from the payment processor (e.g., Stripe's evt_ ID). If the same event arrives twice — whether from a processor retry, a network hiccup, or a misconfigured endpoint — the second delivery is acknowledged with a 200 response but silently discarded.

This means your recovery pipeline never processes the same payment failure twice, even if your processor delivers the event multiple times.

Webhook Payload Storage

Raw inbound webhook payloads are stored encrypted in Cloudflare R2 (AES-256-GCM) for up to 548 days. This retention window covers the full Visa and Mastercard chargeback dispute period.

Stored payloads serve two purposes:

  • Dispute evidence — If a customer disputes a recovered charge, the original failure event and all subsequent recovery actions are available as evidence.
  • Audit trail — For compliance and debugging, the exact payload received from the payment processor is preserved in its original form.

Payloads are retrievable via LostChurn's internal API for the full 548-day retention window. After expiration, payloads are permanently deleted. Customer PII within payloads is subject to the same GDPR erasure policies as all other stored data.

Best Practices

PracticeDetails
Verify signaturesAlways verify the HMAC signature before processing a webhook
Use raw bodyVerify the signature against the raw request body, not a parsed/re-serialized version
Handle duplicatesUse the event id to deduplicate. LostChurn may send the same event more than once
Respond quicklyReturn 200 immediately and process asynchronously
Monitor failuresSet up alerts for webhook delivery failures

Testing Webhooks

You can send test webhook events from the dashboard:

  1. Go to Settings > Webhooks and select an endpoint.
  2. Click Send Test Event.
  3. Select the event type.
  4. Click Send. The test event is delivered to your endpoint with a test: true field in the payload.

For local development, use a tunneling service like ngrok to expose your local server:

ngrok http 8080

Then configure the ngrok URL as a webhook endpoint in LostChurn.

Next Steps

On this page