REFERENCE
Webhook events
The 18 event types NoChurn sends to your webhook endpoints, with what each means and an example payload.
The event envelope
Every delivery is one event: id (nevt_…, the same on every retry and replay: deduplicate on it), object
("event"), type, livemode, created (Unix time) and data, whose content depends on the type.
Customers in data carry their three ids (id, stripe_customer_id, external_id), null when absent.
How to receive and verify them: Webhooks.
Subscribe an endpoint to the types it needs, or to * for all of them. The examples are test-mode data.
All types
| Type | When |
|---|---|
recovery.opened | A payment failed and NoChurn started recovering it. Not sent for customers held out of the flow. |
recovery.recovered | The failed invoice was paid during recovery. |
recovery.lost | Recovery ended without the invoice being paid. |
outcome.confirmed | The result of a flow run was confirmed. |
outcome.reverted | A result reported earlier for a flow run was reverted. |
cancel_session.started | A customer opened the cancel flow. |
cancel_session.offer_accepted | The customer accepted a retention offer in the cancel flow. |
cancel_session.completed | The cancel session ended: see cancel_session.outcome (saved or cancelled). |
customer.access_restricted | The customer must lose access to your app: a payment is still failing after the grace period, or the plan is paused with the wall on. Same truth as GET /v1/customers/{id}/access. |
customer.access_restored | The customer gets access to your app again. |
customer.scored | A customer received a churn score. |
customer.flagged | An early churn signal was raised for a customer, for example a card removed before the renewal. |
winback.reactivated | A customer who had churned came back during a win-back flow. |
billing.retry_requested | Billing sent by API: your billing must attempt the payment of the invoice now, then report the result with POST /v1/payments. Use idempotency_key for the attempt. |
billing.cancel_requested | Billing sent by API: your billing must cancel the subscription. reason is customer_cancelled (the customer cancelled in the cancel flow; at_period_end says when) or recovery_lost (recovery failed: cancel at the end of the period). |
billing.discount_requested | Billing sent by API: the customer accepted a discount offer in the cancel flow; apply it to the subscription. |
billing.pause_requested | Billing sent by API: the customer accepted a pause offer in the cancel flow; pause the subscription. |
billing.downgrade_requested | Billing sent by API: the customer accepted a downgrade offer in the cancel flow; move the subscription to the offered plan. |
Failed payments
The recovery flow, from the failed payment to its end.
recovery.opened
A payment failed and NoChurn started recovering it. Not sent for customers held out of the flow.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "recovery.opened",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": {
"id": "ninv_2Kq8sP4mVx9LdT3a",
"external_id": "in_5521",
"stripe_invoice_id": null,
"status": "open",
"amount_due_cents": 4900,
"amount_remaining_cents": 4900,
"currency": "eur",
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"external_id": "sub_8812",
"stripe_subscription_id": null
}
},
"reason": "Payment failed: insufficient_funds",
"flow_run": {
"id": "run_6Tq1wZ8cHn3Lp5Vd",
"flow": "recovery",
"state": "running",
"outcome": null,
"confirmation": null,
"holdout": false,
"started_at": "2026-12-01T09:00:00Z",
"outcome_at": null
}
}
} recovery.recovered
The failed invoice was paid during recovery.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "recovery.recovered",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": {
"id": "ninv_2Kq8sP4mVx9LdT3a",
"external_id": "in_5521",
"stripe_invoice_id": null,
"status": "open",
"amount_due_cents": 4900,
"amount_remaining_cents": 4900,
"currency": "eur",
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"external_id": "sub_8812",
"stripe_subscription_id": null
}
},
"reason": "Invoice paid after retry 2",
"flow_run": {
"id": "run_6Tq1wZ8cHn3Lp5Vd",
"flow": "recovery",
"state": "ended",
"outcome": "recovered",
"confirmation": null,
"holdout": false,
"started_at": "2026-12-01T09:00:00Z",
"outcome_at": "2026-12-03T10:00:00Z"
}
}
} recovery.lost
Recovery ended without the invoice being paid.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "recovery.lost",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": {
"id": "ninv_2Kq8sP4mVx9LdT3a",
"external_id": "in_5521",
"stripe_invoice_id": null,
"status": "open",
"amount_due_cents": 4900,
"amount_remaining_cents": 4900,
"currency": "eur",
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"external_id": "sub_8812",
"stripe_subscription_id": null
}
},
"reason": "Recovery ended after 4 retries",
"flow_run": {
"id": "run_6Tq1wZ8cHn3Lp5Vd",
"flow": "recovery",
"state": "ended",
"outcome": "lost",
"confirmation": null,
"holdout": false,
"started_at": "2026-12-01T09:00:00Z",
"outcome_at": "2026-12-03T10:00:00Z"
}
}
} Outcomes
The result of a flow run is confirmed, or a result reported earlier is reverted.
outcome.confirmed
The result of a flow run was confirmed.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "outcome.confirmed",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": {
"id": "ninv_2Kq8sP4mVx9LdT3a",
"external_id": "in_5521",
"stripe_invoice_id": null,
"status": "open",
"amount_due_cents": 4900,
"amount_remaining_cents": 4900,
"currency": "eur",
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"external_id": "sub_8812",
"stripe_subscription_id": null
}
},
"reason": "Paid invoice not refunded after 30 days",
"flow_run": {
"id": "run_6Tq1wZ8cHn3Lp5Vd",
"flow": "recovery",
"state": "ended",
"outcome": "recovered",
"confirmation": "confirmed",
"holdout": false,
"started_at": "2026-12-01T09:00:00Z",
"outcome_at": "2026-12-03T10:00:00Z"
}
}
} outcome.reverted
A result reported earlier for a flow run was reverted.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "outcome.reverted",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": {
"id": "ninv_2Kq8sP4mVx9LdT3a",
"external_id": "in_5521",
"stripe_invoice_id": null,
"status": "open",
"amount_due_cents": 4900,
"amount_remaining_cents": 4900,
"currency": "eur",
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"external_id": "sub_8812",
"stripe_subscription_id": null
}
},
"reason": "Invoice refunded",
"flow_run": {
"id": "run_6Tq1wZ8cHn3Lp5Vd",
"flow": "recovery",
"state": "ended",
"outcome": "recovered",
"confirmation": "reverted",
"holdout": false,
"started_at": "2026-12-01T09:00:00Z",
"outcome_at": "2026-12-03T10:00:00Z"
}
}
} Cancel sessions
A customer opens the cancel flow, accepts an offer or cancels.
cancel_session.started
A customer opened the cancel flow.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "cancel_session.started",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"customer_ref": null,
"reason": "Opened from the account page",
"cancel_session": {
"id": "ses_5Wn9kD2rQx7Mb4Lf",
"source": "widget",
"step": "reason",
"outcome": null,
"holdout": false,
"reason": null,
"offer": null,
"offer_accepted": null,
"saved_mrr_cents": null,
"cancel_effective_at": null,
"subscription_source": "token"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
}
}
} cancel_session.offer_accepted
The customer accepted a retention offer in the cancel flow.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "cancel_session.offer_accepted",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"customer_ref": null,
"reason": "Accepted 30% off for 3 months",
"cancel_session": {
"id": "ses_5Wn9kD2rQx7Mb4Lf",
"source": "widget",
"step": "done",
"outcome": null,
"holdout": false,
"reason": "too_expensive",
"offer": "off_3Lm8qT2xVb7Nd4Ks",
"offer_accepted": true,
"saved_mrr_cents": 4900,
"cancel_effective_at": null,
"subscription_source": "token"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
}
}
} cancel_session.completed
The cancel session ended: see cancel_session.outcome (saved or cancelled).
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "cancel_session.completed",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"customer_ref": null,
"reason": "Saved with 30% off for 3 months",
"cancel_session": {
"id": "ses_5Wn9kD2rQx7Mb4Lf",
"source": "widget",
"step": "done",
"outcome": "saved",
"holdout": false,
"reason": "too_expensive",
"offer": "off_3Lm8qT2xVb7Nd4Ks",
"offer_accepted": null,
"saved_mrr_cents": 4900,
"cancel_effective_at": null,
"subscription_source": "token"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
}
}
} Access
Your app must block, or give back, access to a customer.
customer.access_restricted
The customer must lose access to your app: a payment is still failing after the grace period, or the plan is paused with the wall on. Same truth as GET /v1/customers/{id}/access.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "customer.access_restricted",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"reason": "Payment still failing after the 7-day grace period",
"access": "restricted",
"invoice": "ninv_2Kq8sP4mVx9LdT3a",
"access_reason": "payment_failed"
}
} customer.access_restored
The customer gets access to your app again.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "customer.access_restored",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"reason": "Invoice paid",
"access": "full",
"invoice": "ninv_2Kq8sP4mVx9LdT3a",
"access_reason": null
}
} Churn score
A customer received a churn score, or an early churn signal was raised.
customer.scored
A customer received a churn score.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "customer.scored",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"score": 74,
"level": "high",
"flag": null,
"reason": "Score 74: prepaid card, no activity in 21 days"
}
} customer.flagged
An early churn signal was raised for a customer, for example a card removed before the renewal.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "customer.flagged",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"score": 74,
"level": "high",
"flag": "card_removed",
"reason": "Card removed 4 days before the renewal"
}
} Win-back
A customer who had churned came back during a win-back flow.
winback.reactivated
A customer who had churned came back during a win-back flow.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "winback.reactivated",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": null,
"reason": "New subscription 34 days after cancelling",
"flow_run": {
"id": "run_6Tq1wZ8cHn3Lp5Vd",
"flow": "winback",
"state": "ended",
"outcome": "won_back",
"confirmation": null,
"holdout": false,
"started_at": "2026-12-01T09:00:00Z",
"outcome_at": "2026-12-03T10:00:00Z"
}
}
} Billing by API: orders for your backend
Sent only when NoChurn cannot act in a billing tool itself (billing by API): your backend applies them, once per idempotency key.
billing.retry_requested
Billing sent by API: your billing must attempt the payment of the invoice now, then report the result with POST /v1/payments. Use idempotency_key for the attempt.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "billing.retry_requested",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"invoice": {
"id": "ninv_2Kq8sP4mVx9LdT3a",
"external_id": "in_5521",
"stripe_invoice_id": null,
"status": "open",
"amount_due_cents": 4900,
"amount_remaining_cents": 4900,
"currency": "eur",
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"external_id": "sub_8812",
"stripe_subscription_id": null
}
},
"attempt": 2,
"idempotency_key": "nochurn:retry:ninv_2Kq8sP4mVx9LdT3a:2",
"reason": "Payday retry: the 1st of the month, 10:00 local time",
"decline_code": "insufficient_funds"
}
} billing.cancel_requested
Billing sent by API: your billing must cancel the subscription. reason is customer_cancelled (the customer cancelled in the cancel flow; at_period_end says when) or recovery_lost (recovery failed: cancel at the end of the period).
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "billing.cancel_requested",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
},
"at_period_end": true,
"reason": "customer_cancelled",
"cancel_session": "ses_5Wn9kD2rQx7Mb4Lf",
"idempotency_key": "nochurn:cancel_session:ses_5Wn9kD2rQx7Mb4Lf:cancel"
}
} billing.discount_requested
Billing sent by API: the customer accepted a discount offer in the cancel flow; apply it to the subscription.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "billing.discount_requested",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
},
"offer": {
"kind": "discount",
"name": "30% off for 3 months",
"params": {
"percent": 30,
"months": 3
},
"id": "off_3Lm8qT2xVb7Nd4Ks"
},
"cancel_session": "ses_5Wn9kD2rQx7Mb4Lf",
"idempotency_key": "nochurn:offer:off_3Lm8qT2xVb7Nd4Ks:apply"
}
} billing.pause_requested
Billing sent by API: the customer accepted a pause offer in the cancel flow; pause the subscription.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "billing.pause_requested",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
},
"offer": {
"kind": "pause",
"name": "Pause for 2 months",
"params": {
"months": 2
},
"id": "off_3Lm8qT2xVb7Nd4Ks"
},
"cancel_session": "ses_5Wn9kD2rQx7Mb4Lf",
"idempotency_key": "nochurn:offer:off_3Lm8qT2xVb7Nd4Ks:apply"
}
} billing.downgrade_requested
Billing sent by API: the customer accepted a downgrade offer in the cancel flow; move the subscription to the offered plan.
Example payload
{
"id": "nevt_9xQ2mV7pLk4Rt8Ws",
"object": "event",
"type": "billing.downgrade_requested",
"livemode": false,
"created": 1798675200,
"data": {
"customer": {
"id": "cst_4fJ8sK2pQ9wLm3Xz",
"stripe_customer_id": null,
"external_id": "u_123",
"email": "jenny@example.com"
},
"subscription": {
"id": "nsub_7Hd2kQ9xLm3VbN7p",
"stripe_subscription_id": null,
"external_id": "sub_8812",
"provider_subscription_id": null,
"plan": "Pro monthly"
},
"offer": {
"kind": "downgrade",
"name": "Switch to Starter",
"params": {
"plan_name": "Starter monthly"
},
"id": "off_3Lm8qT2xVb7Nd4Ks"
},
"cancel_session": "ses_5Wn9kD2rQx7Mb4Lf",
"idempotency_key": "nochurn:offer:off_3Lm8qT2xVb7Nd4Ks:apply"
}
}