Webhook: cancellation reason

When a subscription is canceled, the SUBSCRIPTION_CANCELED webhook now tells you why. A new top-level cancellation object arrives alongside the existing subscription_contract data, carrying the reason label and any free text the subscriber typed.
What the new field looks like
The payload gains one additive key:
{
"subscription_contract": { ... },
"cancellation": {
"reasons": [{ "title": "Too expensive" }],
"custom_reason": "shipping was always late"
}
}
reasons is always present and never null. Each entry is the label from your cancellation flow or reason list — for example "Too expensive" or "Pausing for now". An empty array means no reason was recorded: a merchant- or API-initiated cancellation, a shop without a cancellation flow, or a subscriber who submitted none.
custom_reason carries the free text the subscriber typed, or null if they typed nothing. The field is never an empty string — a blank submission and no submission are both null, so your code never has to branch on "".
This shape is consistent regardless of how you collect cancellation reasons: a Subi cancellation flow and a per-shop reason list produce the same structure on the webhook side.
What this enables
You can now branch your Klaviyo flows — or any webhook consumer — on the cancellation reason without a support lookup. A subscriber who chose "Too expensive" is a candidate for a discount win-back sequence; one who chose "Pausing for now" is more likely to return on a pause-and-return flow; one who said nothing may be a lapsed-payment case worth a different touch.
The reason data was already captured in Subi — it simply was not serialized into the webhook payload. No change is required to your webhook endpoint configuration or signing setup.