Webhook Data Reference

Overview

This technical reference documents the complete data schema for Churn Buster webhook events. Use this guide to write SQL queries, build dbt models, and create analytics dashboards.

New to Churn Buster webhooks? Start with the Fivetran Setup Guide to get connected first.

Event Types

Churn Buster sends six event types via webhooks:

Event Type Category When It Fires
campaign_started Dunning When a failed payment triggers a new dunning campaign
campaign_won Dunning When payment is successfully recovered
campaign_lost Dunning When campaign ends without recovery (customer churns)
cancel_session_won Cancel Flow When customer is retained via cancel flow offer
cancel_session_lost Cancel Flow When customer completes cancellation
email_bounced Email When campaign email fails delivery (bounces)

Base Event Structure

Every webhook event follows this top-level structure:

{  "account_uuid": "550e8400-e29b-41d4-a716-446655440000",  "event_type": "campaign_won",  "event": {    "customer": { ... },    "campaign": { ... }  // or "cancel_session" or "bounce"  }}

Schema Reference

Customer Object

Present in all events. Contains customer identification and metadata.

"customer": {  "churn_buster_id": "uuid",  "email": "customer@example.com",  "processor": "stripe",  "processor_id": "cus_abc123",  "properties": {    "first_name": "Sarah",    "last_name": "Jones",    "plan": "premium"    // Any custom properties you've sent to Churn Buster  },  "capture_url": "https://pages.churnbuster.io/..."}

Field Descriptions:

Field Type Description
churn_buster_id UUID Unique Churn Buster customer identifier
email String Customer email address
processor String Payment processor name (e.g., "stripe", "braintree", "recharge")
processor_id String Customer ID in your payment processor
properties Object Custom customer attributes you've sent to Churn Buster
capture_url String Shareable URL for customer to update payment method

Campaign Object (Dunning Events)

Used in: campaign_started, campaign_won, campaign_lost

"campaign": {  "amount": {    "cents": 4999,    "currency": "USD",    "formatted": "$49.99"  },  "churn_buster_id": "uuid",  "started_at": "2024-01-15T10:30:00Z",  "finished_at": "2024-01-18T14:20:00Z",  // only in won/lost  "processor": "stripe",  "processor_id": "ch_abc123",  "properties": {}  // Custom campaign properties}

Field Descriptions:

Field Type Description
amount.cents Integer Failed payment amount in cents
amount.currency String Currency code (ISO 4217)
amount.formatted String Human-readable amount with currency symbol
churn_buster_id UUID Unique campaign identifier
started_at ISO 8601 When campaign began
finished_at ISO 8601 When campaign ended (only in won/lost events)
processor String Payment processor name
processor_id String Failed payment/charge ID in processor

Cancel Session Object (Cancel Flow Events)

Used in: cancel_session_won, cancel_session_lost

"cancel_session": {  "amount": {    "cents": 2999,    "currency": "USD",    "formatted": "$29.99"  },  "churn_buster_id": "uuid",  "processor_subscription_id": "sub_abc123",  "cancel_flow": "Default Cancel Flow",  "started_at": "2024-01-15T10:30:00Z",  "finished_at": "2024-01-15T10:35:00Z",  "properties": {},    // ONLY in cancel_session_won:  "offer": {    "offer_id": "uuid",    "offer_name": "10% Off Next 3 Months",    "offer_type": "discount",    "offer_variant_id": "uuid",    "offer_variant_name": "Variant A",    "discount_code": "SAVE10"  // for discount offers  },    // ONLY in cancel_session_lost:  "reason": "I have too much product",  "feedback": {    "question": "How was your experience?",    "answer": "Great service, just need to pause",    "sentiment": "positive"  }}

Bounce Object (Email Bounced Events)

Used only in: email_bounced

"bounce": {  "bounced_at": "2024-01-15T10:30:00Z",  "can_activate": false,  "description": "The recipient's email server permanently rejected the email.",  "details": "550 5.1.1 No such user",  "email": "invalid@example.com",  "inactive": true,  "type": "Hard Bounce"}

SQL Query Examples

1. Monthly Recovery Rate & Revenue

Calculate recovery performance by month:

WITH monthly_campaigns AS (  SELECT    DATE_TRUNC('month', CAST(event->'campaign'->>'started_at' AS TIMESTAMP)) AS month,    COUNT(DISTINCT CASE WHEN event_type = 'campaign_started'       THEN event->'campaign'->>'churn_buster_id' END) AS campaigns_started,    COUNT(DISTINCT CASE WHEN event_type = 'campaign_won'       THEN event->'campaign'->>'churn_buster_id' END) AS campaigns_won,    SUM(CASE WHEN event_type = 'campaign_won'       THEN CAST(event->'campaign'->'amount'->>'cents' AS NUMERIC) / 100       END) AS revenue_recovered  FROM churn_buster.events  WHERE event_type IN ('campaign_started', 'campaign_won')  GROUP BY 1)SELECT  month,  campaigns_started,  campaigns_won,  ROUND(100.0 * campaigns_won / NULLIF(campaigns_started, 0), 2) AS recovery_rate_pct,  ROUND(revenue_recovered, 2) AS total_recoveredFROM monthly_campaignsORDER BY month DESC;

2. Time to Recovery Analysis

How long does it take to recover failed payments?

WITH campaign_lifecycle AS (  SELECT    event->'campaign'->>'churn_buster_id' AS campaign_id,    event->'customer'->>'email' AS customer_email,    MIN(CASE WHEN event_type = 'campaign_started'       THEN CAST(event->'campaign'->>'started_at' AS TIMESTAMP)       END) AS started_at,    MAX(CASE WHEN event_type = 'campaign_won'       THEN CAST(event->'campaign'->>'finished_at' AS TIMESTAMP)       END) AS recovered_at,    MAX(CASE WHEN event_type = 'campaign_won'      THEN CAST(event->'campaign'->'amount'->>'cents' AS NUMERIC) / 100      END) AS amount  FROM churn_buster.events  WHERE event_type IN ('campaign_started', 'campaign_won')  GROUP BY 1, 2  HAVING recovered_at IS NOT NULL)SELECT  DATE_TRUNC('week', started_at) AS week,  COUNT(*) AS recoveries,  ROUND(AVG(EXTRACT(EPOCH FROM (recovered_at - started_at)) / 3600), 1)     AS avg_hours_to_recovery,  ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP     (ORDER BY EXTRACT(EPOCH FROM (recovered_at - started_at)) / 3600), 1)     AS median_hours,  ROUND(SUM(amount), 2) AS total_recoveredFROM campaign_lifecycleGROUP BY 1ORDER BY 1 DESC;

3. Cancel Flow Offer Performance

Which offers are most effective?

SELECT  event->'cancel_session'->'offer'->>'offer_type' AS offer_type,  event->'cancel_session'->'offer'->>'offer_name' AS offer_name,  COUNT(*) AS times_accepted,  SUM(CAST(event->'cancel_session'->'amount'->>'cents' AS NUMERIC)) / 100     AS total_mrr_retained,  ROUND(AVG(CAST(event->'cancel_session'->'amount'->>'cents' AS NUMERIC)) / 100, 2)     AS avg_subscription_valueFROM churn_buster.eventsWHERE event_type = 'cancel_session_won'  AND event->'cancel_session'->'offer' IS NOT NULLGROUP BY 1, 2ORDER BY times_accepted DESC;

4. Top Cancellation Reasons

Why are customers leaving?

SELECT  event->'cancel_session'->>'reason' AS cancellation_reason,  COUNT(*) AS times_selected,  ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) AS percentage,  COUNT(CASE WHEN event->'cancel_session'->'feedback'->>'sentiment' = 'positive'     THEN 1 END) AS positive_feedback,  COUNT(CASE WHEN event->'cancel_session'->'feedback'->>'sentiment' = 'negative'     THEN 1 END) AS negative_feedbackFROM churn_buster.eventsWHERE event_type = 'cancel_session_lost'  AND event->'cancel_session'->>'reason' IS NOT NULLGROUP BY 1ORDER BY times_selected DESCLIMIT 10;

5. Email Deliverability Health

Monitor bounce rates over time:

SELECT  DATE_TRUNC('week', CAST(event->'bounce'->>'bounced_at' AS TIMESTAMP)) AS week,  event->'bounce'->>'type' AS bounce_type,  COUNT(*) AS bounce_count,  COUNT(DISTINCT event->'customer'->>'email') AS unique_customers,  ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER     (PARTITION BY DATE_TRUNC('week', CAST(event->'bounce'->>'bounced_at' AS TIMESTAMP))), 2)     AS pct_of_weekly_bouncesFROM churn_buster.eventsWHERE event_type = 'email_bounced'GROUP BY 1, 2ORDER BY 1 DESC, 3 DESC;

6. Customer Cohort Performance

Compare recovery rates by customer attributes (requires joining with your customer data):

WITH customer_campaigns AS (  SELECT    c.customer_id,    c.signup_date,    c.plan_tier,    e.event_type,    e.event->'campaign'->>'churn_buster_id' AS campaign_id,    CAST(e.event->'campaign'->'amount'->>'cents' AS NUMERIC) / 100 AS amount  FROM churn_buster.events e  JOIN your_schema.customers c     ON c.email = e.event->'customer'->>'email'  WHERE e.event_type IN ('campaign_started', 'campaign_won'))SELECT  plan_tier,  DATE_TRUNC('quarter', signup_date) AS cohort,  COUNT(DISTINCT CASE WHEN event_type = 'campaign_started'     THEN campaign_id END) AS campaigns,  COUNT(DISTINCT CASE WHEN event_type = 'campaign_won'     THEN campaign_id END) AS recoveries,  ROUND(100.0 * COUNT(DISTINCT CASE WHEN event_type = 'campaign_won' THEN campaign_id END) /     NULLIF(COUNT(DISTINCT CASE WHEN event_type = 'campaign_started' THEN campaign_id END), 0), 2)     AS recovery_rate_pctFROM customer_campaignsGROUP BY 1, 2ORDER BY 2 DESC, 1;

dbt Model Examples

Staging Model: Flatten Events

-- models/staging/stg_churn_buster__events.sql{{  config(    materialized='incremental',    unique_key='event_id'  )}}WITH source AS (  SELECT * FROM {{ source('churn_buster', 'events') }}  {% if is_incremental() %}  WHERE _fivetran_synced > (SELECT MAX(synced_at) FROM {{ this }})  {% endif %}),parsed AS (  SELECT    {{ dbt_utils.generate_surrogate_key(['account_uuid', 'event_type', 'event']) }}       AS event_id,    account_uuid,    event_type,    _fivetran_synced AS synced_at,        -- Customer fields    event->'customer'->>'churn_buster_id' AS customer_id,    event->'customer'->>'email' AS customer_email,    event->'customer'->>'processor' AS processor,    event->'customer'->>'processor_id' AS processor_customer_id,        -- Campaign fields (for dunning events)    CASE WHEN event_type IN ('campaign_started', 'campaign_won', 'campaign_lost')      THEN event->'campaign'->>'churn_buster_id' END AS campaign_id,    CASE WHEN event_type IN ('campaign_started', 'campaign_won', 'campaign_lost')      THEN CAST(event->'campaign'->'amount'->>'cents' AS NUMERIC) / 100       END AS campaign_amount,    CASE WHEN event_type IN ('campaign_started', 'campaign_won', 'campaign_lost')      THEN CAST(event->'campaign'->>'started_at' AS TIMESTAMP)       END AS campaign_started_at,    CASE WHEN event_type IN ('campaign_won', 'campaign_lost')      THEN CAST(event->'campaign'->>'finished_at' AS TIMESTAMP)       END AS campaign_finished_at,        -- Cancel session fields    CASE WHEN event_type IN ('cancel_session_won', 'cancel_session_lost')      THEN event->'cancel_session'->>'churn_buster_id' END AS cancel_session_id,    CASE WHEN event_type IN ('cancel_session_won', 'cancel_session_lost')      THEN CAST(event->'cancel_session'->'amount'->>'cents' AS NUMERIC) / 100       END AS cancel_session_amount,    CASE WHEN event_type = 'cancel_session_won'      THEN event->'cancel_session'->'offer'->>'offer_name'       END AS accepted_offer_name,    CASE WHEN event_type = 'cancel_session_lost'      THEN event->'cancel_session'->>'reason'       END AS cancellation_reason        FROM source)SELECT * FROM parsed

Mart Model: Campaign Performance

-- models/marts/fct_campaign_performance.sqlWITH campaigns AS (  SELECT    campaign_id,    customer_id,    customer_email,    processor,    campaign_amount,    campaign_started_at,    MAX(campaign_finished_at) AS campaign_finished_at,    MAX(CASE WHEN event_type = 'campaign_won' THEN 1 ELSE 0 END) AS is_won,    MAX(CASE WHEN event_type = 'campaign_lost' THEN 1 ELSE 0 END) AS is_lost  FROM {{ ref('stg_churn_buster__events') }}  WHERE event_type IN ('campaign_started', 'campaign_won', 'campaign_lost')  GROUP BY 1, 2, 3, 4, 5, 6),final AS (  SELECT    campaign_id,    customer_id,    customer_email,    campaign_amount,    campaign_started_at,    campaign_finished_at,    CASE      WHEN is_won = 1 THEN 'won'      WHEN is_lost = 1 THEN 'lost'      ELSE 'active'    END AS campaign_status,    CASE WHEN is_won = 1 THEN campaign_amount ELSE 0 END AS revenue_recovered,    EXTRACT(EPOCH FROM (campaign_finished_at - campaign_started_at)) / 3600       AS hours_to_resolution  FROM campaigns)SELECT * FROM final

Common Use Cases

For Finance Teams

  • Track recovered revenue vs. at-risk revenue
  • Forecast cash flow based on recovery patterns
  • Monitor accounts receivable resolution rates
  • Calculate customer lifetime value including recovery impact

For Customer Success Teams

  • Identify at-risk customers when campaigns start
  • Understand churn reasons from cancel flow data
  • Monitor payment health as part of customer health scores
  • Proactively reach out to high-value failed payments

For Product Teams

  • A/B test cancel flow offers and measure impact
  • Monitor email deliverability and bounce rates
  • Correlate product usage with recovery/retention rates
  • Analyze feature adoption among retained vs. churned customers

For Marketing Teams

  • Segment customers by acquisition channel and recovery rate
  • Analyze cohort performance over time
  • Calculate true customer acquisition ROI (factoring in recovery)
  • Identify patterns in high-risk customer segments

Best Practices

💡 Query Optimization Tips

  • Create indexes on event_type and timestamp fields for faster filtering
  • Use incremental dbt models to avoid reprocessing all historical data
  • Pre-aggregate common metrics into summary tables for dashboard performance
  • Consider partitioning tables by month if you have high event volume

Sample Payloads

To view complete sample payloads for all event types, visit your Churn Buster webhook settings page:

View Sample Payloads →

This page shows the exact JSON structure for each event type, including all nested objects and fields.

Need Help?