Event contract
Fields the query expects
| Field | Type | Why it exists |
|---|---|---|
| timestamp_utc | Timestamp | Invoice event time. |
| invoice_id | Utf8 | Stable provider invoice identifier. |
| event_name | Utf8 | invoice_payment_failed or invoice_paid. |
| amount_usd | Float64 | Normalized invoice amount. |
| failure_type | Utf8 | Safe categorized failure reason. |
Copy the query
WITH failures AS (
SELECT
invoice_id,
MIN(timestamp_utc) AS first_failed_at,
MAX(amount_usd) AS amount_usd,
MIN(failure_type) AS failure_type
FROM invoice_events
WHERE event_name = 'invoice_payment_failed'
AND timestamp_utc >= now() - INTERVAL '60 days'
GROUP BY invoice_id
),
recoveries AS (
SELECT
invoice_id,
MIN(timestamp_utc) AS recovered_at
FROM invoice_events
WHERE event_name = 'invoice_paid'
GROUP BY invoice_id
)
SELECT
failures.failure_type,
COUNT(*) AS failed_invoices,
SUM(CASE
WHEN recoveries.recovered_at > failures.first_failed_at THEN 1
ELSE 0
END) AS recovered_invoices,
100.0 * SUM(CASE
WHEN recoveries.recovered_at > failures.first_failed_at THEN 1
ELSE 0
END) / NULLIF(COUNT(*), 0) AS recovery_rate_pct,
SUM(CASE
WHEN recoveries.recovered_at IS NULL THEN failures.amount_usd
ELSE 0.0
END) AS unrecovered_amount_usd
FROM failures
LEFT JOIN recoveries
ON recoveries.invoice_id = failures.invoice_id
GROUP BY failures.failure_type
ORDER BY unrecovered_amount_usd DESC;This read-only query is planned and executed against an empty typed table with Apache DataFusion 45.2.0. The deterministic sample output is synthetic and reviewed separately; validate field types, thresholds, and business definitions against your own data. Read the testing methodology.
Query result
Payment recovery rate by failure type
Authentication-required failures recover least often and retain meaningful revenue risk.
| failure_type | failed_invoices | recovered_invoices | recovery_rate_pct | unrecovered_amount_usd |
|---|---|---|---|---|
| insufficient_funds | 142 | 89 | 62.68 | 6,840 |
| expired_card | 58 | 47 | 81.03 | 1,320 |
| authentication_required | 36 | 18 | 50 | 2,940 |
Synthetic example output. Run the query against your own event schema and thresholds before using it for operational decisions.
How the SQL works
- 1The failure CTE collapses repeated provider attempts into one invoice at risk.
- 2A recovery counts only when payment succeeds after the first recorded failure.
- 3Unrecovered amount complements the rate so a small number of large invoices remains visible.
Edge cases to decide
- Deduplicate webhook deliveries before treating them as new payment attempts.
- Refunds and chargebacks need separate lifecycle events after a successful recovery.
- Normalize currency with a documented rate and retain original currency fields for reconciliation.
Recommended dashboard
- Bars: recovery_rate_pct by failure_type
- Stat: unrecovered_amount_usd
- Table: highest-value unrecovered invoices and account owner
Alert guidance
Alert revenue operations when unrecovered value exceeds a threshold or a normally recoverable failure type deteriorates.
Read alert setupPut the recipe to work
Related instrumentation and guides
Continue the analysis
Measure Webhook Retry Recovery
Separate permanent webhook failures from deliveries that recovered on a later attempt.
Open recipeCalculate Trial-to-Paid Conversion
Measure how many trial accounts become paid customers within a fixed conversion window.
Open recipeCalculate Monthly Recurring Revenue Movement
Separate new, expansion, contraction, churn, and reactivation MRR from billing lifecycle events.
Open recipeRun it on real events
Create a table, adapt the fields, and save the result
Start free, send structured events, and use the query result as a chart, shared dashboard widget, or alert input.