This page documents every event type Recurr emits. All events share the common envelope documented in 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:
{
"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) |
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.
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) |
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.