This is the complete data reference for Churn Buster webhook events. Use it to write SQL queries, build dbt models, and create dashboards in your BI tool.
New to Churn Buster webhooks? Start with the Data Warehouse Setup Guide to get connected first.
Churn Buster sends six event types via webhooks:
Every webhook event has the same top-level structure:
{
"event_type": "campaign_won",
"event": {
"customer": { ... },
"campaign": { ... }
}
}
The contents of event depend on the event type:
campaign and customercancel_session and customercampaign, bounce, and customerPresent in all event types.
{
"churn_buster_id": "uuid",
"email": "customer@example.com",
"processor": "stripe",
"processor_id": "cus_000000001",
"properties": {
"first_name": "Sarah",
"last_name": "Jones"
},
}
Present in: campaign_started, campaign_won, campaign_lost, email_bounced
{
"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",
"processor": "stripe",
"processor_id": "ch_000000001",
"properties": {}
}
Present in: cancel_session_won, cancel_session_lost
{
"amount": {
"cents": 2999,
"currency": "USD",
"formatted": "$29.99"
},
"churn_buster_id": "uuid",
"processor_subscription_id": "sub_000000001",
"cancel_flow": "Default Cancel Flow",
"started_at": "2024-01-15T10:30:00Z",
"finished_at": "2024-01-15T10:35:00Z",
"properties": {}
}
For cancel_session_won events, the session also includes an offer object (see below).
For cancel_session_lost events, the session also includes:
question, answer, and sentiment fields (see below).{
"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"
}
Present when feedback was collected. May be an empty object if the customer didn't provide feedback.
{
"question": "How was your experience?",
"answer": "It was great!",
"sentiment": "positive"
}
Present in: email_bounced (alongside the campaign object)
{
"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"
}
The queries below use PostgreSQL JSON operators (-> and ->>) to access fields from raw webhook payloads. If your data warehouse tool flattens the JSON into columns automatically (e.g., Fivetran's "Unpacked" format), you can reference those columns directly instead and won't need the JSON operators.
To see your actual column names, run a simple SELECT * FROM your_table LIMIT 5 after data starts flowing.
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_recovered
FROM monthly_campaigns
ORDER BY month DESC;
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 MAX(CASE
WHEN event_type = 'campaign_won'
THEN CAST(event->'campaign'->>'finished_at' AS TIMESTAMP)
END) 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_recovered
FROM campaign_lifecycle
GROUP BY 1
ORDER BY 1 DESC;
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_value
FROM churn_buster.events
WHERE event_type = 'cancel_session_won'
AND event->'cancel_session'->'offer' IS NOT NULL
GROUP BY 1, 2
ORDER BY times_accepted DESC;
SELECT
event->'cancel_session'->>'reason' AS cancellation_reason,
COUNT(*) AS times_selected,
ROUND(
100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 2
) AS percentage
FROM churn_buster.events
WHERE event_type = 'cancel_session_lost'
AND event->'cancel_session'->>'reason' IS NOT NULL
GROUP BY 1
ORDER BY times_selected DESC
LIMIT 10;
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
FROM churn_buster.events
WHERE event_type = 'email_bounced'
GROUP BY 1, 2
ORDER BY 1 DESC, 3 DESC;
File: 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,
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,
CASE WHEN event_type IN (
'campaign_started', 'campaign_won', 'campaign_lost', 'email_bounced'
)
THEN event->'campaign'->>'churn_buster_id'
END AS campaign_id,
CASE WHEN event_type IN (
'campaign_started', 'campaign_won', 'campaign_lost', 'email_bounced'
)
THEN CAST(event->'campaign'->'amount'->>'cents' AS NUMERIC) / 100
END AS campaign_amount,
CASE WHEN event_type IN (
'campaign_started', 'campaign_won', 'campaign_lost', 'email_bounced'
)
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,
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_won'
THEN event->'cancel_session'->'offer'->>'offer_type'
END AS accepted_offer_type,
CASE WHEN event_type = 'cancel_session_lost'
THEN event->'cancel_session'->>'reason'
END AS cancellation_reason
FROM source
)
SELECT * FROM parsed
File: models/marts/fct_campaign_performance.sql
WITH 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
To view complete sample payloads for every event type with your account's actual processor details, visit your webhook settings page in Churn Buster:
Settings → Integrations → Webhooks → Webhook Sample Payloads
This shows the exact JSON structure for each event, including all nested objects and conditional fields.