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

# RevenueCat

> Send RevenueCat purchase, renewal, cancellation and refund events to AppDNA through an authenticated webhook, so they count in revenue, lifetime value and attribution.

## Overview

When RevenueCat is your purchase SDK, RevenueCat — not AppDNA — owns every store transaction. AppDNA learns about
purchases, renewals, cancellations and refunds from **RevenueCat's webhook**. Each RevenueCat event is converted into
AppDNA's own canonical events (`purchase_completed`, `subscription_renewed`, …) with `source: "revenuecat"`, so they
appear in revenue, lifetime value, attribution, audiences and dashboards exactly like purchases made through AppDNA's own
billing.

<Info>
  **RevenueCat event mapping is enabled at release.** Authenticated RevenueCat deliveries are counted as soon as the
  integration is connected.
</Info>

<Note>
  **Provider events count for revenue, not for activity.** RevenueCat events count for revenue, lifetime revenue and
  attribution. They never count as sessions, active users, retention or billable MAU — those come only from the AppDNA SDK
  running in your app. RevenueCat purchases and renewals do count toward MTPU (monthly tracked paying users), once per
  user after de-duplication with SDK purchases.
</Note>

## Configure the SDK for RevenueCat

Set `billingProvider` to `revenueCat` in your AppDNA configuration. With that setting:

* AppDNA **never finishes (iOS) or acknowledges (Android)** a store transaction — RevenueCat does.
* RevenueCat is not linked into AppDNA itself (on Android adding it to your app does not change that; for iOS see [when a provider counts as linked](/sdks/ios/billing#who-owns-the-transaction)), so in every published build tapping a plan on an **AppDNA paywall** does not open a store sheet. The SDK emits `purchase_failed` with
  `error_type: "providerNotAvailable"` and calls your paywall delegate's `onPaywallPurchaseFailed` with
  `errorType: "providerNotAvailable"` and the tapped `productId`.
* For the same reason, `AppDNA.billing.restorePurchases()` throws `providerNotAvailable` in every published build: restore through RevenueCat
  (`Purchases.restorePurchases`).
* The SDK does not emit device-side `subscription_renewed`, `subscription_canceled` or `subscription_renewal_failed`
  events: the webhook is the single source for them.

**Recipe — buy from an AppDNA paywall with RevenueCat:** in `onPaywallPurchaseFailed`, when `errorType` is
`providerNotAvailable`, start the purchase for `productId` with RevenueCat (`Purchases.purchase(...)`), then dismiss the
paywall. The purchase then reaches AppDNA through this webhook.

Use RevenueCat's `CustomerInfo` as the source of truth for entitlements; the SDK's own entitlement reads come from the
store and can differ (for example, RevenueCat promotional grants).

## Identity

Call RevenueCat's `Purchases.logIn` with **the same user id** you pass to `AppDNA.identify`. AppDNA matches each
RevenueCat event to the AppDNA user whose `identify` id equals RevenueCat's app user id (or one of its aliases).
Events from an anonymous RevenueCat user are still stored and counted, and are linked to the user once a later event
carries an identified app user id.

## Do not track purchases yourself

Once the webhook is connected, **do not track purchase or subscription events yourself with `AppDNA.track`**
(`purchase_completed`, `subscription_started`, …). The webhook is the purchase source; a host-tracked copy that does not
carry RevenueCat's transaction id would be counted a second time. For UI analytics around your own purchase screens, use custom event names (for example
`checkout_sheet_opened`).

## Connect

1. Open **Integrations** in the dashboard, choose **RevenueCat → Connect**, enter your RevenueCat **Secret API Key**
   and press **Test & Connect** (admin role).
2. The dialog shows the **Webhook URL**. If you are the organisation **owner**, it also shows the **Authorization header
   value** once, with Copy. An admin who is not the owner sees "Ask your organisation owner to reveal the webhook
   credential on this integration's page".
3. In RevenueCat → **Project settings → Integrations → Webhooks**, add a webhook:
   * **Webhook URL:** the URL from AppDNA.
   * **Authorization header value:** the value from AppDNA (it has the form `Bearer <secret>`; AppDNA also accepts the
     secret without the `Bearer ` prefix).
   * Environment: send both sandbox and production events (AppDNA separates them, below).
4. Press **Send test event** in RevenueCat. AppDNA answers **200** — the test event is stored but never counted.

### Reveal and rotate (owner only)

On the integration's page the Authorization value is masked. Only the organisation owner can **Reveal** and **Copy** it,
or **Rotate** it. Rotating invalidates the old value **immediately**: RevenueCat deliveries fail with 401 until you paste
the new value into RevenueCat. RevenueCat retries a failed delivery up to five times, 5, 10, 20, 40 and 80 minutes apart
(about 2 hours 35 minutes in all), and then stops, so deliveries
made during the paste window are not lost. Disconnecting and reconnecting the integration keeps the existing value.

### Optional: webhook signing secret

RevenueCat can also sign each delivery (header `X-RevenueCat-Webhook-Signature`). To require signatures:

1. In RevenueCat → Webhooks, enable signing and copy the signing secret (RevenueCat shows it once).
2. On the AppDNA integration page, paste it into **Webhook signing secret (optional)** and save (owner only).

Once a signing secret is set, **both** the Authorization value and the signature must be valid, and a signature older
than 5 minutes is rejected. If you set the secret in AppDNA but do not enable signing in RevenueCat, every delivery is
rejected. **Remove** turns signature verification off again.

## Sandbox and production

RevenueCat marks every event `SANDBOX` or `PRODUCTION`. Sandbox events are written to AppDNA's **sandbox** dataset and
never reach production revenue, dashboards or subscription analytics.

## Event mapping

| RevenueCat event | AppDNA event |
| - | - |
| `INITIAL_PURCHASE` | `purchase_completed`, plus `subscription_started` for a subscription. A trial start has `is_trial: true` and price 0 |
| `NON_RENEWING_PURCHASE` | `purchase_completed` |
| `RENEWAL` | `subscription_renewed` (with price, revenue and `is_trial_conversion`) |
| `CANCELLATION` that is a refund (`cancel_reason` `CUSTOMER_SUPPORT`, or a negative price) | `purchase_refunded` (negative amount) |
| `CANCELLATION` otherwise (auto-renew off, billing error, …) | `subscription_canceled` (`cancel_reason` passed through) |
| `EXPIRATION` | `subscription_expired` |
| `BILLING_ISSUE` | `subscription_renewal_failed` |
| `REFUND_REVERSED` | `purchase_refunded` with a positive amount and `refund_reversed: true` |
| `TRANSFER` | not counted as revenue. When `transferred_from` holds an anonymous RevenueCat App User ID and `transferred_to` an identified one, it is published as `subscription_transferred` (no price, no revenue), which links the anonymous user's earlier RevenueCat events to the identified user in analytics; otherwise it is stored and not published. It moves a subscription to the identified user it was transferred to only when the event carries a product id, an expiration and the original transaction id of an App Store, Google Play or Stripe subscription AppDNA already has from earlier RevenueCat events; otherwise it changes no subscription (a `TRANSFER` never creates a subscription) |
| `SUBSCRIBER_ALIAS` | not counted as an event. Under the same condition as `TRANSFER` it re-points that subscription to the event's user |
| `UNCANCELLATION`, `PRODUCT_CHANGE`, `SUBSCRIPTION_PAUSED`, `SUBSCRIPTION_EXTENDED`, `TEMPORARY_ENTITLEMENT_GRANT`, `INVOICE_ISSUANCE`, `VIRTUAL_CURRENCY_TRANSACTION`, `EXPERIMENT_ENROLLMENT`, `PURCHASE_REDEEMED`, price-increase consent events | stored, not counted (no money moves, or the money arrives in a later mapped event) |
| `TEST` | stored as a test, never counted |
| Any future event type | stored, not counted |

Every published event carries `environment`, `source: "revenuecat"` and AppDNA's bookkeeping keys (`provider`,
`rc_event_type`, `provider_placeholder_ids` and `_appdna_origin`, and `rc_event_id` / `provider_stable_id` when known). Every event except
`subscription_transferred` also carries `is_trial`, plus, when RevenueCat sends them,
`product_id` (for Google Play, the subscription id before the `:` of RevenueCat's `<subscription>:<base plan>`),
`store_product_id` (the full RevenueCat product id), `transaction_id`, `original_transaction_id`, `store`, `price`
(RevenueCat's `price_in_purchased_currency`), `currency`, `period_type`, `country_code` and `entitlement_ids`;
`subscription_transferred` adds only `store`. Only the three events that move money — `purchase_completed`, `subscription_renewed` and
`purchase_refunded` — carry `revenue_usd` and count as revenue; on `subscription_started`, `subscription_canceled`,
`subscription_expired` and `subscription_renewal_failed` the price is informational and is never counted. Retries of the
same RevenueCat event are deduplicated, and a purchase that both RevenueCat and another source report is counted once.
Renewals of a subscription RevenueCat reports are counted from RevenueCat only: the AppDNA SDK's own
`subscription_renewed` for that subscription is kept as an event but adds no revenue, even when its transaction id differs.

## Troubleshooting

| Symptom | Cause |
| - | - |
| RevenueCat shows 401 for every delivery | The Authorization value in RevenueCat does not match AppDNA's (pasted wrongly, or rotated in AppDNA) — reveal and paste again |
| 401 only since you set a signing secret | Signing is not enabled in RevenueCat, or its signing secret was rotated there and not pasted into AppDNA |
| Deliveries answer 200 but nothing is counted | The integration is **disconnected** — deliveries are acknowledged and dropped by design while it is disconnected |
| Deliveries answer 200 with `reason: "integration_error"` | The Secret API Key check failed and the integration is in an error state. Deliveries are held (stored, not counted) — press **Test & Connect** with a working key; once it connects, the held deliveries are processed and counted |
| Purchases appear under an anonymous user | `Purchases.logIn` was not called with the `AppDNA.identify` id |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.