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

# Integrating Flex with RevenueCat

>  Connect Flex with RevenueCat to sync subscription events, pass custom identifiers, and keep customer status up to date.

## 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

<div className="mx-auto w-full rounded-xl overflow-hidden relative mt-6 mb-6" style={{ paddingTop: '56.25%' }}>
  <iframe className="absolute top-0 left-0 w-full h-full" src="https://www.youtube.com/embed/SD5QBFrlHuE?si=pJeBIvOLKZKZ6dXr" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />
</div>

<Steps>
  <Step title="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**.

          <img src="https://mintcdn.com/flex-87/3K46z-DtjrhVQlsr/revenuecat-add-app.png?fit=max&auto=format&n=3K46z-DtjrhVQlsr&q=85&s=3dc1a134a974288e38a2eafef9b9b02d" alt="Flex Dashboard → Integrations page with RevenueCat selected" width="1830" height="358" data-path="revenuecat-add-app.png" />
  </Step>

  <Step title="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.

    <img src="https://mintcdn.com/flex-87/3K46z-DtjrhVQlsr/revenuecat-app-id-api-key.png?fit=max&auto=format&n=3K46z-DtjrhVQlsr&q=85&s=0d2d9703398a4e879a4ef90884cfd7a5" alt="RevenueCat “Apps” page showing Other Payment Provider selected" width="1824" height="1000" data-path="revenuecat-app-id-api-key.png" />

    <img src="https://mintcdn.com/flex-87/3K46z-DtjrhVQlsr/revenuecat-other-payment-provider.png?fit=max&auto=format&n=3K46z-DtjrhVQlsr&q=85&s=0a505a2bc7440ae22feb68eb5de45dce" alt="RevenueCat “Apps” page showing Other Payment Provider selected" width="1818" height="194" data-path="revenuecat-other-payment-provider.png" />

    <Warning>
      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
    </Warning>
  </Step>

  <Step title="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`.

    ```json theme={null}
    {
      "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"
        }
      }
    }
    ```
  </Step>

  <Step title="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.
  </Step>
</Steps>

***

## How Flex Integrates with RevenueCat

There are two ways subscriptions can flow into RevenueCat:

<CardGroup cols={2}>
  <Card icon="stripe-s" title="Native Stripe Integration">
    Stripe sends webhooks directly to RC. RC processes them and generates events with store: `"STRIPE"`.
  </Card>

  <Card icon="bolt" title="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"`.
  </Card>
</CardGroup>

<Warning>
  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.
</Warning>

<Info>
  **If you're migrating from a native Stripe → RevenueCat integration**, be aware of these key differences:

  * **`store` is `"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_reason` is always `null`** — use `cancel_reason` on preceding CANCELLATION events instead
  * **Prorated refunds are invisible in webhooks** — no `CUSTOMER_SUPPORT` event 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.
</Info>

***

## 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) |

<Note>
  `ProductChange` (upgrades/downgrades) is **not currently supported**. All other subscription lifecycle events are fully implemented and synced in real-time.
</Note>

***

## 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

<AccordionGroup>
  <Accordion icon="cart-shopping" title="New Subscription (No Trial)">
    | # | RC Event Type      | cancel\_reason | price    | gives\_access |
    | - | ------------------ | -------------- | -------- | ------------- |
    | 1 | `INITIAL_PURCHASE` | —              | \$179.99 | true          |
  </Accordion>

  <Accordion title="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`.
  </Accordion>

  <Accordion icon="arrows-rotate" title="Renewal">
    | # | RC Event Type | price    | gives\_access |
    | - | ------------- | -------- | ------------- |
    | 1 | `RENEWAL`     | \$179.99 | true          |
  </Accordion>

  <Accordion icon="triangle-exclamation" title="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.
  </Accordion>

  <Accordion icon="calendar-xmark" title="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.
  </Accordion>

  <Accordion icon="rotate-left" title="Undo Cancel / Re-subscribe Before Period Ends">
    | # | RC Event Type    | gives\_access | auto\_renewal\_status |
    | - | ---------------- | ------------- | --------------------- |
    | 1 | `UNCANCELLATION` | true          | will\_renew           |
  </Accordion>

  <Accordion title="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         |

    <Warning>
      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.
    </Warning>
  </Accordion>

  <Accordion title="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          |

    <Warning>
      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.
    </Warning>
  </Accordion>

  <Accordion icon="hourglass-end" title="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.
  </Accordion>
</AccordionGroup>

***

## 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](#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               |

<Info>
  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.
</Info>

***

## 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)  |

<Tip>
  **Recommended:** Use Flex webhooks for refund tracking (exact amounts and types) and RC webhooks for subscription lifecycle (active, canceled, expired, billing).
</Tip>

***

## Recommendations for Webhook Consumers

### Handling the store field

Accept events where store is either `"STRIPE"` or `"EXTERNAL"`:

```python theme={null}
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

<Steps>
  <Step title="Detect refund occurrence">
    Check for `cancel_reason == "CUSTOMER_SUPPORT"` on CANCELLATION events.
  </Step>

  <Step title="Do NOT rely on negative price">
    The price will always be `0.0` for External store events.
  </Step>

  <Step title="Get refund amounts">
    Use the Flex API or RC Subscriber API for exact refund amounts.
  </Step>

  <Step title="Handle prorated/custom refunds">
    No RC webhook event is generated for these — use Flex webhooks instead.
  </Step>
</Steps>

### 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.

<Tabs>
  <Tab title="INITIAL_PURCHASE (Trial Start)">
    ```json theme={null}
    {
      "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": {}
      }
    }
    ```
  </Tab>

  <Tab title="INITIAL_PURCHASE (Conversion)">
    ```json theme={null}
    {
      "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
      }
    }
    ```
  </Tab>

  <Tab title="RENEWAL">
    ```json theme={null}
    {
      "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
      }
    }
    ```
  </Tab>

  <Tab title="CANCELLATION (User)">
    ```json theme={null}
    {
      "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
      }
    }
    ```
  </Tab>

  <Tab title="CANCELLATION (Refund)">
    ```json theme={null}
    {
      "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
      }
    }
    ```

    <Note>
      Price is `0.0`, not negative. Revenue is adjusted inside RC but not reflected in this webhook payload.
    </Note>
  </Tab>

  <Tab title="UNCANCELLATION">
    ```json theme={null}
    {
      "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
      }
    }
    ```
  </Tab>

  <Tab title="EXPIRATION">
    ```json theme={null}
    {
      "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
      }
    }
    ```
  </Tab>

  <Tab title="BILLING_ISSUE">
    ```json theme={null}
    {
      "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
      }
    }
    ```
  </Tab>
</Tabs>

***

## FAQ

<AccordionGroup>
  <Accordion title="Why is store EXTERNAL instead of STRIPE?">
    Flex uses RevenueCat's External Purchases API to sync subscription data. RC automatically sets store to `"EXTERNAL"` for all events processed through this API. This is expected behavior, not a misconfiguration.
  </Accordion>

  <Accordion title="Why is price 0.0 on refund events instead of negative?">
    The External Purchases API does not expose payment amounts in the webhook price field. Flex sends the correct negative payment to RC, and revenue IS adjusted correctly inside RC. But the webhook event's price field is derived from the subscription record, not the payment. This is a confirmed RevenueCat platform limitation (verified March 2026).
  </Accordion>

  <Accordion title="How do I know the refund amount?">
    Three options:

    1. **RC Subscriber API:** `GET /v1/subscribers/{app_user_id}` — check `total_revenue_in_usd`
    2. **Flex Webhooks:** Use Flex's own webhook events, which include exact refund amounts and types
    3. **Flex API:** Query the Flex API for subscription/refund details
  </Accordion>

  <Accordion title="Why is there no refund event for prorated refunds?">
    RevenueCat only generates a `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.
  </Accordion>

  <Accordion title="Why is expiration_reason null?">
    The External Purchases API does not have a mechanism to set the expiration reason. This field is only populated for native integrations (Apple, Google, Stripe). To determine why a subscription expired, check the `cancel_reason` on the preceding CANCELLATION event.
  </Accordion>

  <Accordion title="Is revenue correctly tracked despite these differences?">
    Yes. Revenue (including refund adjustments) is always accurate in RC. The differences are only in webhook event payloads, not in RC's internal data. You can always query `GET /v1/subscribers/{id}` to get accurate revenue figures.
  </Accordion>

  <Accordion title="Does Flex support trial subscriptions?">
    Yes. Flex syncs both `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).
  </Accordion>

  <Accordion title="Does Flex support undo-cancel (re-subscribe)?">
    Yes. When `cancel_at_period_end` is reversed, Flex sends an `UndoCancelAtPeriodEnd` event which appears as `UNCANCELLATION` in RC webhooks.
  </Accordion>

  <Accordion title="Does Flex handle billing failures and recovery?">
    Yes. `BillingIssue` is sent when payment fails (status: `in_grace_period`, `gives_access: true`). `BillingSucceeds` is sent when payment recovers (same logic as Renewal).
  </Accordion>

  <Accordion title="Will these differences change in the future?">
    These are inherent to the RevenueCat External Purchases API as of March 2026. If RC updates the External API to support additional fields, Flex will adopt them. This document will be updated accordingly.
  </Accordion>
</AccordionGroup>
