Overview
Flex integrates with RevenueCat to help you manage subscriptions seamlessly across platforms. This guide walks you through connecting your RevenueCat account to Flex, passing a stable customer identifier, saving the integration, and understanding how events map between systems.Installation Guide
Add RevenueCat in Flex
-
You can find these in your RevenueCat project under App Settings.

Configure in RevenueCat


Pass a Custom User Identifier to RevenueCat
metadata as REVENUE_CAT_CUSTOMER_ID.How Flex Integrates with RevenueCat
There are two ways subscriptions can flow into RevenueCat:Native Stripe Integration
"STRIPE".Flex External Purchases API
POST /v1/receipts/external). RC generates events with store: "EXTERNAL".storeis"EXTERNAL", not"STRIPE"— update any webhook consumers that filter by store- Refund events have
price: 0.0, never negative — use the RC Subscriber API or Flex API for refund amounts expiration_reasonis alwaysnull— usecancel_reasonon preceding CANCELLATION events instead- Prorated refunds are invisible in webhooks — no
CUSTOMER_SUPPORTevent is generated; use Flex API to detect partial refunds - IDs use Flex format (
fprod_,fsub_,fcus_) instead of Stripe format (prod_,si_,cus_) — treat all IDs as opaque strings
Subscription Lifecycle Events
Flex syncs the following RevenueCat event types. This is the complete list — every event type that Flex sends to RC:| Flex Internal Event | RC Webhook Event Type | Description | Includes Payment? |
|---|---|---|---|
TrialPurchase | INITIAL_PURCHASE | Free trial started ($0 invoice). Subscription created with status: trialing. | No |
TrialConversion | INITIAL_PURCHASE | Trial converted to paid subscription. First real payment processed. | Yes (positive amount) |
Renewal | RENEWAL | Subscription renewed at start of a new billing period. | Yes (positive amount) |
BillingSucceeds | RENEWAL | Payment recovered after a past_due period. Same logic as Renewal. | Yes (positive amount) |
BillingIssue | BILLING_ISSUE | Payment failed during renewal. Subscription enters grace period. | No |
Expiration | EXPIRATION | Subscription access ended. Triggered by Stripe customer.subscription.deleted. | No |
CancelAtPeriodEnd | CANCELLATION | Subscription set to not renew at end of current period. Still active. | No |
UndoCancelAtPeriodEnd | UNCANCELLATION | Cancel-at-period-end reversed. Subscription will renew again. | No |
Refund | CANCELLATION | Refund issued. Sends expired status + negative payment to adjust revenue. | Yes (negative amount) |
ProductChange (upgrades/downgrades) is not currently supported. All other subscription lifecycle events are fully implemented and synced in real-time.Stripe Trigger Mapping
Each Flex RC sync event is triggered by a specific Stripe webhook event:| Stripe Webhook Event | Condition | Flex RC Event Type |
|---|---|---|
invoice.payment_succeeded | amount_paid == 0 AND status == trialing | TrialPurchase |
invoice.payment_succeeded | status == trialing AND amount > 0 | TrialConversion |
invoice.payment_succeeded | billing_reason == subscription_create | TrialConversion |
invoice.payment_succeeded | status == active, billing cycle | Renewal |
invoice.payment_succeeded | status == past_due | BillingSucceeds |
invoice.payment_failed | Any | BillingIssue |
customer.subscription.updated | cancel_at_period_end: false → true | CancelAtPeriodEnd |
customer.subscription.updated | cancel_at_period_end: true → false | UndoCancelAtPeriodEnd |
customer.subscription.deleted | Any | Expiration |
charge.refund.updated | refund status == succeeded | Refund |
Event Sequences by Scenario
New Subscription (No Trial)
New Subscription (No Trial)
| # | RC Event Type | cancel_reason | price | gives_access |
|---|---|---|---|---|
| 1 | INITIAL_PURCHASE | — | $179.99 | true |
New Subscription (With Free Trial)
New Subscription (With Free Trial)
| # | RC Event Type | Trigger | price | status |
|---|---|---|---|---|
| 1 | INITIAL_PURCHASE | Trial starts ($0 invoice) | $0.00 | trialing |
| 2 | INITIAL_PURCHASE | Trial converts (first payment) | $179.99 | active |
INITIAL_PURCHASE in RC webhooks. The first has price 0 and status trialing; the second has the actual price and status active.Renewal
Renewal
| # | RC Event Type | price | gives_access |
|---|---|---|---|
| 1 | RENEWAL | $179.99 | true |
Payment Failure → Recovery
Payment Failure → Recovery
| # | RC Event Type | Trigger | status | gives_access |
|---|---|---|---|---|
| 1 | BILLING_ISSUE | Payment fails | in_grace_period | true (grace period) |
| 2 | RENEWAL | Payment retried and succeeds | active | true |
EXPIRATION event when Stripe deletes it.Cancel at End of Billing Period (Option A)
Cancel at End of Billing Period (Option A)
| # | RC Event Type | cancel_reason | gives_access | auto_renewal_status |
|---|---|---|---|---|
| 1 | CANCELLATION | UNSUBSCRIBE | true | will_not_renew |
Undo Cancel / Re-subscribe Before Period Ends
Undo Cancel / Re-subscribe Before Period Ends
| # | RC Event Type | gives_access | auto_renewal_status |
|---|---|---|---|
| 1 | UNCANCELLATION | true | will_renew |
Cancel Immediately + Full Refund (Option B)
Cancel Immediately + Full Refund (Option B)
| # | RC Event Type | cancel_reason / expiration_reason | price | gives_access |
|---|---|---|---|---|
| 1 | CANCELLATION | cancel_reason: UNSUBSCRIBE | $0.00 | true |
| 2 | EXPIRATION | expiration_reason: null | $0.00 | false |
| 3 | CANCELLATION | cancel_reason: CUSTOMER_SUPPORT | $0.00 | false |
Cancel Immediately + Prorated Refund (Option C)
Cancel Immediately + Prorated Refund (Option C)
| # | RC Event Type | cancel_reason / expiration_reason | price | gives_access |
|---|---|---|---|---|
| 1 | EXPIRATION | expiration_reason: null | $0.00 | false |
| 2 | CANCELLATION | cancel_reason: UNSUBSCRIBE | $0.00 | true |
Subscription Expires Naturally (After Cancel at Period End)
Subscription Expires Naturally (After Cancel at Period End)
| # | RC Event Type | expiration_reason | gives_access |
|---|---|---|---|
| 1 | EXPIRATION | null | false |
CancelAtPeriodEnd was set.Field Reference
Every RC webhook event from Flex will have these field values:| Field | Value | Notes |
|---|---|---|
store | "EXTERNAL" | Always EXTERNAL for Flex-managed subscriptions |
environment | "SANDBOX" or "PRODUCTION" | Matches the Flex partner’s test_mode setting |
product_id | fprod_xxx | Flex product ID (not a Stripe product ID) |
transaction_id | fsub_xxx | Flex subscription ID |
original_transaction_id | fsub_xxx | Same as transaction_id |
app_user_id | fcus_xxx | Flex customer ID |
currency | USD | ISO 4217 currency code |
price | 0.0 or positive amount | Never negative — see Refund Handling |
price_in_purchased_currency | Same as price | Always matches price |
commission_percentage | 0.0 | Flex handles its own payment processing |
takehome_percentage | 1.0 | Flex handles its own payment processing |
tax_percentage | 0.0 | Tax reported as zero |
country_code | null | Not available via External API |
entitlement_ids | null or populated | Depends on RC project entitlement configuration |
renewal_number | 1 | Always 1 — External API limitation |
is_family_share | false | Not applicable |
period_type | "NORMAL" | Always NORMAL |
subscriber_attributes | {} | Empty unless custom attributes configured |
metadata | null | Not populated |
expiration_reason | null | Never populated for External store |
cancel_reason | "UNSUBSCRIBE" or "CUSTOMER_SUPPORT" | UNSUBSCRIBE = user-initiated cancel. CUSTOMER_SUPPORT = refund. |
Key Differences from Stripe-Native Integration
If your webhook consumer was originally built for a native Stripe → RevenueCat integration, the following differences apply when receiving events from Flex. These are inherent to the RevenueCat External Purchases API and have been confirmed with the RevenueCat team (March 2026).| Field | Stripe-Native Value | Flex External Value | Why | Impact |
|---|---|---|---|---|
store | "STRIPE" | "EXTERNAL" | RC sets this based on app type | Must accept both values |
price (refund) | Negative (e.g. -179.99) | Always 0.0 | External API does not expose payment amounts in webhook price field | Cannot detect refund amount from webhook |
price_in_purchased_currency (refund) | Negative | Always 0.0 | Same as above | Same as above |
expiration_reason | "UNSUBSCRIBE" | null | External API does not set this field | Accept null as valid |
commission_% | ~0.03 (Stripe fee) | 0.0 | Flex handles its own payment processing | Informational only |
takehome_% | ~0.97 | 1.0 | Derived from zero commission | Informational only |
product_id | prod_xxx (Stripe) | fprod_xxx (Flex) | Flex uses its own product IDs | Treat as opaque string |
transaction_id | si_xxx (Stripe sub item) | fsub_xxx (Flex sub) | Flex uses its own subscription IDs | Treat as opaque string |
app_user_id | cus_xxx (Stripe customer) | fcus_xxx (Flex customer) | Flex uses its own customer IDs | Treat as opaque string |
renewal_number | Increments each period | Always 1 | External API does not track renewal count | Track externally if needed |
Refund Handling
Refund visibility in RC webhooks depends on the refund type and flow:| Scenario | RC Webhook Event | cancel_reason | price | Revenue Adjusted in RC? |
|---|---|---|---|---|
| Full refund (immediate cancel + refund) | CANCELLATION | CUSTOMER_SUPPORT | 0.0 (not negative) | Yes — gross = $0.00 |
| Prorated refund | No specific refund event | N/A | N/A | Yes — gross = net amount |
| Custom refund amount | No specific refund event | N/A | N/A | Yes — gross = net amount |
| Cancel at period end then later refund | May not generate CUSTOMER_SUPPORT | N/A | N/A | Yes — always adjusted |
How to detect and measure refunds
| Method | How | Detects Full Refunds? | Detects Prorated Refunds? | Gets Refund Amount? |
|---|---|---|---|---|
| RC Webhook | cancel_reason == "CUSTOMER_SUPPORT" | Yes (2-step flow only) | No | No (price is 0.0) |
| RC Subscriber API | GET /v1/subscribers/{id} → check total_revenue_in_usd | Yes | Yes | Yes (net revenue) |
| Flex API / Webhooks | Flex refund events with amounts, types, timestamps | Yes | Yes | Yes (exact amount) |
Recommendations for Webhook Consumers
Handling the store field
Accept events where store is either"STRIPE" or "EXTERNAL":
Detecting subscription state changes
Use the event type andcancel_reason to determine subscription state:
| Condition | Meaning |
|---|---|
type == INITIAL_PURCHASE | New subscription or trial conversion |
type == RENEWAL | Successful renewal payment or billing recovery |
type == CANCELLATION AND cancel_reason == UNSUBSCRIBE | User canceled (won’t renew, still active) |
type == CANCELLATION AND cancel_reason == CUSTOMER_SUPPORT | Refund issued (full refund only) |
type == UNCANCELLATION | Cancel reversed, will renew again |
type == EXPIRATION | Access revoked, subscription ended |
type == BILLING_ISSUE | Payment failed, in grace period |
Handling refunds
Handling expiration events
Acceptexpiration_reason: null. The External store does not populate this field. To determine why a subscription expired, look at the preceding CANCELLATION event’s cancel_reason.
ID formats
Treat all IDs as opaque strings. Do not parse, validate format, or assume a prefix:| ID Field | Stripe-Native Format | Flex Format |
|---|---|---|
product_id | prod_xxx | fprod_xxx |
transaction_id | si_xxx | fsub_xxx |
original_transaction_id | si_xxx | fsub_xxx |
app_user_id | cus_xxx | fcus_xxx |
Example Payloads
All payloads below are from real webhook captures in the RevenueCat sandbox.- INITIAL_PURCHASE (Trial Start)
- INITIAL_PURCHASE (Conversion)
- RENEWAL
- CANCELLATION (User)
- CANCELLATION (Refund)
- UNCANCELLATION
- EXPIRATION
- BILLING_ISSUE
FAQ
Why is store EXTERNAL instead of STRIPE?
Why is store EXTERNAL instead of STRIPE?
"EXTERNAL" for all events processed through this API. This is expected behavior, not a misconfiguration.Why is price 0.0 on refund events instead of negative?
Why is price 0.0 on refund events instead of negative?
How do I know the refund amount?
How do I know the refund amount?
- RC Subscriber API:
GET /v1/subscribers/{app_user_id}— checktotal_revenue_in_usd - Flex Webhooks: Use Flex’s own webhook events, which include exact refund amounts and types
- Flex API: Query the Flex API for subscription/refund details
Why is there no refund event for prorated refunds?
Why is there no refund event for prorated refunds?
CANCELLATION (CUSTOMER_SUPPORT) event for full refunds processed through the External API. Prorated and custom refunds adjust revenue internally but do not trigger a distinct webhook event. Use the Flex API or RC Subscriber API to detect partial refunds.Why is expiration_reason null?
Why is expiration_reason null?
cancel_reason on the preceding CANCELLATION event.Is revenue correctly tracked despite these differences?
Is revenue correctly tracked despite these differences?
GET /v1/subscribers/{id} to get accurate revenue figures.Does Flex support trial subscriptions?
Does Flex support trial subscriptions?
TrialPurchase (trial start, $0 invoice) and TrialConversion (first real payment after trial). Both appear as INITIAL_PURCHASE in RC webhooks — differentiate by checking price (0.0 for trial start, positive for conversion).Does Flex support undo-cancel (re-subscribe)?
Does Flex support undo-cancel (re-subscribe)?
cancel_at_period_end is reversed, Flex sends an UndoCancelAtPeriodEnd event which appears as UNCANCELLATION in RC webhooks.Does Flex handle billing failures and recovery?
Does Flex handle billing failures and recovery?
BillingIssue is sent when payment fails (status: in_grace_period, gives_access: true). BillingSucceeds is sent when payment recovers (same logic as Renewal).Will these differences change in the future?
Will these differences change in the future?