> ## Documentation Index
> Fetch the complete documentation index at: https://recurr.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Full catalog of Recurr webhook events — subscription, payment, motion, ticket. Per-event payload schemas + example deliveries.

This page documents every event type Recurr emits. All events share the common envelope documented in [Overview](/api-reference/overview); each event's `data` field is documented per type below.

Event types follow the convention `<resource>.<action>`:

* `subscription.*` — subscription lifecycle
* `payment.*` — individual charge events
* `motion.*` — lifecycle motion conversions
* `ticket.*` — subscriber-submitted tickets

***

## Subscription events

### `subscription.activated`

A subscription becomes active for the first time. Triggers:

* New web sub from direct-web acquisition
* Existing app-store sub successfully migrated to web

**`data` fields:**

| Field                    | Type            | Description                                                                                         |
| ------------------------ | --------------- | --------------------------------------------------------------------------------------------------- |
| `source`                 | string          | `"direct_web"` or `"migration"`                                                                     |
| `cohort_id`              | string \| null  | Cohort ID if the activation came via a Recurr-orchestrated motion                                   |
| `attribution_source`     | string \| null  | Acquisition channel for `direct_web` (e.g., `"google_ads"`, `"tiktok_ads"`, `"organic"`, `"email"`) |
| `first_payment_amount`   | integer         | Activation payment amount in smallest currency unit                                                 |
| `first_payment_currency` | string          | ISO 4217 currency code                                                                              |
| `trial_days_remaining`   | integer \| null | Days remaining in trial if activated under a trial; null if direct paid activation                  |

**Example:**

```json theme={null}
{
  "id": "evt_01HQX8K9M1P0R5N3Y2T7B4C6V",
  "type": "subscription.activated",
  "schema_version": "v1",
  "created_at": "2026-05-22T12:34:56Z",
  "tenant": { "id": "tnt_app123", "name": "ExampleApp" },
  "subscriber": { "id": "subscriber_01HQ...", "email": "user@example.com", "email_hashed": "sha256:abc...", "created_at": "2025-03-10T00:00:00Z" },
  "subscription": { "id": "sub_01HQ...", "status": "active", "plan": "premium_monthly", "current_period_start": "2026-05-22T00:00:00Z", "current_period_end": "2026-06-22T00:00:00Z" },
  "data": {
    "source": "migration",
    "cohort_id": "cohort_q3_pilot",
    "attribution_source": null,
    "first_payment_amount": 999,
    "first_payment_currency": "USD",
    "trial_days_remaining": null
  }
}
```

### `subscription.renewed`

A subscription renews — charge succeeded on an existing subscription.

**`data` fields:**

| Field              | Type    | Description                                              |
| ------------------ | ------- | -------------------------------------------------------- |
| `renewal_count`    | integer | Number of renewals to date (excludes initial activation) |
| `payment_amount`   | integer | Renewal amount in smallest currency unit                 |
| `payment_currency` | string  | ISO 4217 currency code                                   |
| `next_renewal_at`  | string  | ISO 8601 timestamp of expected next renewal              |

### `subscription.upgraded`

A subscription changes to a higher-priced plan or longer billing interval. Triggers:

* Annual nudge motion converts (monthly → annual)
* Plan switch within the billing portal (Pro → Premium)
* Cancel save flow accepts a "switch plan" offer

**`data` fields:**

| Field              | Type    | Description                                                                           |
| ------------------ | ------- | ------------------------------------------------------------------------------------- |
| `from_plan`        | string  | Previous plan identifier                                                              |
| `to_plan`          | string  | New plan identifier                                                                   |
| `trigger`          | string  | `"annual_nudge"`, `"manual_switch"`, `"cancel_save"`, `"plan_change"`                 |
| `proration_amount` | integer | Proration credit/charge applied in smallest currency unit (signed; negative = credit) |
| `new_period_end`   | string  | ISO 8601 timestamp of new period end                                                  |

### `subscription.cancelled`

A subscription enters cancelled state (immediate or scheduled-at-period-end).

**`data` fields:**

| Field                    | Type           | Description                                                                                                                        |
| ------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `cancel_at`              | string         | When the subscription effectively ends (ISO 8601)                                                                                  |
| `at_period_end`          | boolean        | `true` if cancelling at current period end; `false` if immediate                                                                   |
| `cancel_reason`          | string \| null | Reason from exit survey (`"too_expensive"`, `"not_using"`, `"found_alternative"`, `"missing_feature"`, `"life_change"`, `"other"`) |
| `cancel_reason_freetext` | string \| null | Free-text response from exit survey                                                                                                |
| `tenure_days`            | integer        | Subscriber's tenure on Recurr (days since first activation)                                                                        |

<Note>
  Exit-survey fields (`cancel_reason`, `cancel_reason_freetext`) persist on the subscriber's record and are available to subsequent winback motions for personalisation. This is the cleanest example of Recurr's first-party data continuity — capture at cancel, use at winback months later.
</Note>

### `subscription.recovered`

A previously-failed or lapsed subscription returns to active state. Triggers:

* Failed payment recovered via smart retry
* Lapsed sub re-activated via winback motion
* Manual reactivation by customer-facing ops

**`data` fields:**

| Field                       | Type            | Description                                                   |
| --------------------------- | --------------- | ------------------------------------------------------------- |
| `recovery_method`           | string          | `"smart_retry"`, `"winback_motion"`, `"manual_reactivation"`  |
| `days_since_lapse`          | integer \| null | Days between lapse and recovery (null for `smart_retry`)      |
| `recovery_payment_amount`   | integer         | Recovery charge in smallest currency unit                     |
| `recovery_payment_currency` | string          | ISO 4217 currency code                                        |
| `previous_cancel_reason`    | string \| null  | Original cancel reason from exit survey (for winback context) |

***

## Payment events

Payment events fire per individual charge. Distinct from `subscription.renewed` — payment events cover both subscription renewals and non-subscription charges (gift purchases, one-time fees, motion performance fees deducted at settlement).

### `payment.succeeded`

A charge succeeded.

**`data` fields:**

| Field              | Type           | Description                                                                                |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------ |
| `amount`           | integer        | Charge amount in smallest currency unit                                                    |
| `currency`         | string         | ISO 4217 currency code                                                                     |
| `stripe_charge_id` | string         | Stripe charge ID for cross-reference                                                       |
| `charge_type`      | string         | `"subscription_renewal"`, `"subscription_activation"`, `"one_time"`, `"motion_settlement"` |
| `motion_id`        | string \| null | If `charge_type` is `motion_settlement`, the motion ID                                     |

### `payment.failed`

A charge attempt failed.

**`data` fields:**

| Field                | Type           | Description                                                                             |
| -------------------- | -------------- | --------------------------------------------------------------------------------------- |
| `amount`             | integer        | Attempted amount in smallest currency unit                                              |
| `currency`           | string         | ISO 4217 currency code                                                                  |
| `failure_reason`     | string         | Stripe failure code (e.g., `"card_declined"`, `"insufficient_funds"`, `"expired_card"`) |
| `retry_scheduled_at` | string \| null | ISO 8601 timestamp of next smart-retry attempt                                          |
| `attempt_number`     | integer        | Which attempt this was (1 = initial, 2+ = retries)                                      |

### `payment.refunded`

A refund issued against a prior charge.

**`data` fields:**

| Field                | Type           | Description                                                                                  |
| -------------------- | -------------- | -------------------------------------------------------------------------------------------- |
| `refund_amount`      | integer        | Refund amount in smallest currency unit                                                      |
| `refund_currency`    | string         | ISO 4217 currency code                                                                       |
| `refund_reason`      | string \| null | `"customer_request"`, `"goodwill"`, `"chargeback"`, `"duplicate"`, `"fraudulent"`, `"other"` |
| `is_partial`         | boolean        | `true` if partial refund; `false` if full                                                    |
| `original_charge_id` | string         | Stripe charge ID of the original payment                                                     |

***

## Motion events

Motion events fire when a lifecycle motion successfully converts a subscriber. These are the events Recurr earns performance fees against.

### `motion.cancel_save`

A subscriber clicked Cancel, saw the save flow, and accepted a save offer.

**`data` fields:**

| Field                    | Type           | Description                                                                             |
| ------------------------ | -------------- | --------------------------------------------------------------------------------------- |
| `save_offer_type`        | string         | `"pause"`, `"downgrade"`, `"credit"`, `"extend_trial"`, `"sweetener"`                   |
| `original_cancel_reason` | string \| null | Reason captured by exit survey before the save offer was presented                      |
| `save_offer_value`       | integer        | Value of the save offer in smallest currency unit (e.g., credit amount, discount value) |
| `annualised_revenue`     | integer        | Saved sub's annualised revenue for performance-fee calculation                          |

### `motion.winback_recovered`

A previously-cancelled sub reactivated via winback motion.

**`data` fields:**

| Field                     | Type           | Description                                                                                      |
| ------------------------- | -------------- | ------------------------------------------------------------------------------------------------ |
| `cancelled_at`            | string         | When the sub originally cancelled (ISO 8601)                                                     |
| `days_since_cancellation` | integer        | Days between cancellation and winback recovery                                                   |
| `winback_offer`           | string \| null | What converted (`"new_trial"`, `"discount"`, `"feature_announcement"`, `"personalised_message"`) |
| `original_cancel_reason`  | string \| null | Original cancel reason — used to personalise the winback approach                                |
| `annualised_revenue`      | integer        | Recovered sub's annualised revenue for performance-fee calculation                               |

### `motion.annual_upgrade_converted`

A monthly sub upgraded to annual via the annual nudge motion.

**`data` fields:**

| Field                    | Type    | Description                                                                                                  |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------ |
| `from_monthly_payment`   | integer | Previous monthly payment in smallest currency unit                                                           |
| `to_annual_payment`      | integer | New annual payment in smallest currency unit                                                                 |
| `effective_discount_pct` | number  | Effective discount from monthly × 12 to annual price (0 if priced flat)                                      |
| `trigger_signal`         | string  | What triggered the nudge (`"engagement_threshold"`, `"renewal_proximity"`, `"tenure_milestone"`, `"manual"`) |
| `annualised_revenue`     | integer | Equal to `to_annual_payment` for this motion                                                                 |

***

## Ticket events

### `ticket.submitted`

A subscriber submitted a ticket via Recurr's help center or billing portal.

**`data` fields:**

| Field      | Type   | Description                                                                                                                |
| ---------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `topic`    | string | Pre-classified topic — `"billing_question"`, `"refund_request"`, `"plan_change"`, `"cancel"`, `"payment_issue"`, `"other"` |
| `subject`  | string | Subscriber's subject line                                                                                                  |
| `body`     | string | Subscriber's message body                                                                                                  |
| `source`   | string | Where the ticket originated — `"help_center"`, `"billing_portal"`, `"email_reply"`                                         |
| `priority` | string | `"normal"`, `"high"` (auto-elevated for refund requests, payment issues)                                                   |

<Note>
  Pre-classification of `topic` uses Recurr's billing context (which portal page the ticket originated from, the subscriber's recent payment history, etc.). This enriched payload lets your CS tool's AI triage make better routing decisions than a generic ticket capture.
</Note>
