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.
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?