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

TypeWhen
recovery.openedA payment failed and NoChurn started recovering it. Not sent for customers held out of the flow.
recovery.recoveredThe failed invoice was paid during recovery.
recovery.lostRecovery ended without the invoice being paid.
outcome.confirmedThe result of a flow run was confirmed.
outcome.revertedA result reported earlier for a flow run was reverted.
cancel_session.startedA customer opened the cancel flow.
cancel_session.offer_acceptedThe customer accepted a retention offer in the cancel flow.
cancel_session.completedThe cancel session ended: see cancel_session.outcome (saved or cancelled).
customer.access_restrictedThe 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_restoredThe customer gets access to your app again.
customer.scoredA customer received a churn score.
customer.flaggedAn early churn signal was raised for a customer, for example a card removed before the renewal.
winback.reactivatedA customer who had churned came back during a win-back flow.
billing.retry_requestedBilling 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_requestedBilling 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_requestedBilling sent by API: the customer accepted a discount offer in the cancel flow; apply it to the subscription.
billing.pause_requestedBilling sent by API: the customer accepted a pause offer in the cancel flow; pause the subscription.
billing.downgrade_requestedBilling 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"
  }
}