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

  1. 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.
  2. 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:

  1. 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 session metadata as REVENUE_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"
        }
      }
    }
    
  2. 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:

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

  1. Detect refund occurrence
    Check for cancel_reason == "CUSTOMER_SUPPORT" on CANCELLATION events.
  2. Do NOT rely on negative price
    The price will always be 0.0 for External store events.
  3. Get refund amounts
    Use the Flex API or RC Subscriber API for exact refund amounts.
  4. 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.

{
  "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
  }
}