Integrating Flex with RevenueCat - Flex Documentation
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
Flex + RevenueCat Integration - YouTube
Flex + RevenueCat Integration
Add RevenueCat in Flex
Log in to your Flex Dashboard. Go to Integrations → RevenueCat. Enter your RevenueCat App ID and API Key.- You can find these in your RevenueCat project under App Settings.
Configure in RevenueCat
In your RevenueCat project, open the Apps section. Click + Add and select Other payment provider. Copy your App ID and Secret API Key. Paste these values into the Flex Dashboard RevenueCat fields from Step 1.
If you don’t see “Other payment provider (External source)” in your RevenueCat Apps page, that option may not be enabled for your project. Please contact RevenueCat Support and ask them to enable the External payment provider integration for your project. Include:
- Your Project ID / link
- The App name
- That you want to connect Flex as the external payment provider
Pass a Custom User Identifier to RevenueCat
If you want RevenueCat to reference your own user ID (or a Flex user ID), include it in the checkout sessionmetadataasREVENUE_CAT_CUSTOMER_ID.{ "checkout_session": { "line_items": [ { "price": "fprice_01k2j2zfnbrs5e0r4c8wqphv29", "quantity": 1 } ], "success_url": "https://example.com/thank-you?success=true", "mode": "subscription", "cancel_url": "https://example.com/thank-you?canceled=true", "shipping_address_collection": false, "defaults": { "first_name": "Varsha", "last_name": "Parthay", "email": "rclivedemo1@gmail.com" }, "metadata": { "REVENUE_CAT_CUSTOMER_ID": "1234567890" } } }Save and Sync
Click Save in the Flex Dashboard integration page. Flex will begin sending subscription events to RevenueCat so customer status stays in sync.Tip: If you have existing subscriptions, run an initial sync (or perform a backfill) so historical subscriptions are recognized in RevenueCat.
How Flex Integrates with RevenueCat
There are two ways subscriptions can flow into RevenueCat:
Native Stripe Integration
Stripe sends webhooks directly to RC. RC processes them and generates events with store: "STRIPE".
Flex External Purchases API
Flex processes payments via Stripe, then syncs subscription state to RC via the External Purchases API (POST /v1/receipts/external). RC generates events with store: "EXTERNAL".
For Flex-managed subscriptions, all events come through the External Purchases API path. The subscription data is accurate and revenue is correctly tracked — but certain webhook fields have different values than a native Stripe integration would produce.
If you’re migrating from a native Stripe → RevenueCat integration, be aware of these key differences:
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
These differences are inherent to the RevenueCat External Purchases API and have been confirmed with the RevenueCat team (March 2026). Revenue is always accurately tracked inside RevenueCat — these differences only affect webhook payload fields.
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)
| # | RC Event Type | cancel_reason | price | gives_access |
|---|---|---|---|---|
| 1 | INITIAL_PURCHASE |
— | $179.99 | true |
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 |
Both events appear as INITIAL_PURCHASE in RC webhooks. The first has price 0 and status trialing; the second has the actual price and status active.
Renewal
| # | RC Event Type | price | gives_access |
|---|---|---|---|
| 1 | RENEWAL |
$179.99 | true |
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 |
If payment never recovers, the subscription eventually receives an EXPIRATION event when Stripe deletes it.
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 |
Subscription remains active until the current period ends. Access continues during this time.
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)
| # | 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 |
Event #3 is the refund signal. In a native Stripe integration, price would be -179.99. With the External API, the price is always 0.00. Revenue IS correctly adjusted inside RC — query the Subscriber API to confirm net revenue.
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 |
No CANCELLATION (CUSTOMER_SUPPORT) event is generated for prorated refunds. The refund is invisible in webhooks. Revenue is adjusted internally in RC. Use the Flex API or RC Subscriber API for refund details.
Subscription Expires Naturally (After Cancel at Period End)
| # | RC Event Type | expiration_reason | gives_access |
|---|---|---|---|
| 1 | EXPIRATION |
null | false |
This fires when the billing period ends after a 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 |
These differences are not bugs. They are confirmed RevenueCat platform behavior for the External Purchases API. Flex sends all available data correctly; the External API simply exposes different fields than native integrations.
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) |
Recommended: Use Flex webhooks for refund tracking (exact amounts and types) and RC webhooks for subscription lifecycle (active, canceled, expired, billing).
Recommendations for Webhook Consumers
Handling the store field
Accept events where store is either "STRIPE" or "EXTERNAL":
if event.store in ["STRIPE", "EXTERNAL"]:
process_event(event)
Detecting subscription state changes
Use the event type and cancel_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
- Detect refund occurrence
Check forcancel_reason == "CUSTOMER_SUPPORT"on CANCELLATION events. - Do NOT rely on negative price
The price will always be0.0for External store events. - Get refund amounts
Use the Flex API or RC Subscriber API for exact refund amounts. - Handle prorated/custom refunds
No RC webhook event is generated for these — use Flex webhooks instead.
Handling expiration events
Accept expiration_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
{
"api_version": "1.0",
"event": {
"type": "INITIAL_PURCHASE",
"store": "EXTERNAL",
"environment": "SANDBOX",
"product_id": "fprod_xxx",
"transaction_id": "fsub_xxx",
"original_transaction_id": "fsub_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 0.0,
"price_in_purchased_currency": 0.0,
"period_type": "NORMAL",
"renewal_number": 1,
"is_family_share": false,
"commission_percentage": 0.0,
"takehome_percentage": 1.0,
"tax_percentage": 0.0,
"country_code": null,
"entitlement_ids": null,
"expiration_reason": null,
"cancel_reason": null,
"subscriber_attributes": {}
}
}
{
"api_version": "1.0",
"event": {
"type": "INITIAL_PURCHASE",
"store": "EXTERNAL",
"environment": "SANDBOX",
"product_id": "fprod_xxx",
"transaction_id": "fsub_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 179.99,
"price_in_purchased_currency": 179.99,
"commission_percentage": 0.0,
"takehome_percentage": 1.0,
"tax_percentage": 0.0,
"renewal_number": 1
}
}
{
"api_version": "1.0",
"event": {
"type": "RENEWAL",
"store": "EXTERNAL",
"product_id": "fprod_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 179.99,
"price_in_purchased_currency": 179.99,
"commission_percentage": 0.0,
"takehome_percentage": 1.0,
"renewal_number": 1
}
}
{
"api_version": "1.0",
"event": {
"type": "CANCELLATION",
"cancel_reason": "UNSUBSCRIBE",
"store": "EXTERNAL",
"product_id": "fprod_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 0.0,
"price_in_purchased_currency": 0.0,
"commission_percentage": 0.0,
"takehome_percentage": 1.0
}
}
{
"api_version": "1.0",
"event": {
"type": "CANCELLATION",
"cancel_reason": "CUSTOMER_SUPPORT",
"store": "EXTERNAL",
"product_id": "fprod_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 0.0,
"price_in_purchased_currency": 0.0,
"commission_percentage": 0.0,
"takehome_percentage": 1.0
}
}
Price is 0.0, not negative. Revenue is adjusted inside RC but not reflected in this webhook payload.
{
"api_version": "1.0",
"event": {
"type": "UNCANCELLATION",
"store": "EXTERNAL",
"product_id": "fprod_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 0.0,
"commission_percentage": 0.0,
"takehome_percentage": 1.0
}
}
{
"api_version": "1.0",
"event": {
"type": "EXPIRATION",
"expiration_reason": null,
"store": "EXTERNAL",
"product_id": "fprod_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 0.0,
"price_in_purchased_currency": 0.0,
"commission_percentage": 0.0,
"takehome_percentage": 1.0
}
}
{
"api_version": "1.0",
"event": {
"type": "BILLING_ISSUE",
"store": "EXTERNAL",
"product_id": "fprod_xxx",
"app_user_id": "fcus_xxx",
"currency": "USD",
"price": 0.0,
"commission_percentage": 0.0,
"takehome_percentage": 1.0
}
}