Skip to main content

External Purchases API (BETA) (0.1)

Download OpenAPI specification:Download

Introduction

Private Beta
This is a documentation of the External Purchases API, which is currently in private beta.
What is the External Purchases API? When to use it?

Context

The External Purchases API enables you to track subscription status and revenue data from Payment Service Providers that RevenueCat doesn't support natively. Such examples of Payment Service Providers, referenced as External Sources, are Paddle, PayPal, Braintree, Adyen, Recurly, etc.

You can post subscription status and payment events to the External Purchases API to use RevenueCat as a single source of truth for providing access to your application across mobile and web and to consolidate revenue data and key subscription metrics. Customer history, Webhooks and Integration events are supported as well, but they have limitations during the Beta period.

The API expects data to be posted in a format defined in the API reference, meaning that you would need to convert or transform data from the External Source before posting it to the External Purchases API endpoint. RevenueCat accepts data posted to this endpoint as is, and relies on your timely requests to provide entitlement access.

If you have a mobile first business with a web presence that follows a similar monetization model and subscription lifecycle implementation typical to the app stores, then this API is the right choice for you to consolidate your stack.

Adding an External Source

You can add an External Source under Project > Apps > +New. All you need to provide is the App name. The External API Keys and RevenueCat App ID required to call the API are automatically generated after hitting Save Changes.

Authentication

Authentication for the External Purchases API is achieved by setting the Authorization header with a valid API secret external key. You'll find two main types of API keys in your RevenueCat dashboard: public app-specific , secret and secret app-specific.

Authorization: Bearer YOUR_REVENUECAT_APP_SPECIFIC_SECRET_API_KEY

The API keys are automatically generated for each External Source whenever you add a new External Source on the Add New App screen in the Dashboard.

You can retrieve these API keys both on the Apps settings page and Project settings > API Keys page.

Permissions

You can call the External Purchases API only with the corresponding app-specific Secret API key. The app-specific Secret API Key should be kept out of any publicly accessible areas such as GitHub or client side code. Additionally, an app-specific Public API Key is generated so public endpoints like Offerings can be accessed.

Base URL

The base URL for the External Purchases API is https://api.revenuecat.com/.

Endpoints

The API has one endpoint, POST/receipts/external.

POST/receipts/external is used for requests with a single purchase and/or payment, and processes them right away. Calling Get Customer right after will return the updated entitlements.

For more details check the API reference.

Payload

The body of the POST requests should be encoded in JSON and have the 'Content-Type' header set to 'application/json'.

Content-Type: application/json
{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-04-01T00:00:00",
    "current_period_starts_at": "2023-04-01T00:00:00",
    "current_period_ends_at": "2023-05-01T00:00:00",
    "gives_access": true,
    "status": "active",
    "environment": "production",
    "auto_renewal_status": "will_renew"
  },
  "payment": {
    "object": "external_subscription_payment",
    "source_subscription_identifier": "paddle_sub_id1234",
    "payment_identifier": "payment_id1234",
    "processed_at": "2023-04-01T00:00:00",
    "amount_in_local_currency": {
      "gross": 9.99,
      "currency": "USD"
    }
  }
}

Example Subscription Lifecycle

This example of a subscription lifecycle demonstrates the series of API request to make to the External Purchases API and the resulting events generated (for Customer History, Webhooks and Integration Events).

This flow covers a Trial Flow with Successful Conversion, an ordinary Subscription Renewal, a Billing Issue Flow that gets resolved in Grace Period, and eventually a Cancellation Flow.

Sample API requests

Trial purchase

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-03-01T00:00:00",
    "current_period_starts_at": "2023-03-01T00:00:00",
    "current_period_ends_at": "2023-04-01T00:00:00",
    "gives_access": true,
    "status": "trialing",
    "environment": "production"
  },
  "payment": null
}

Trial conversion

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-04-01T00:00:00",
    "current_period_starts_at": "2023-04-01T00:00:00",
    "current_period_ends_at": "2023-05-01T00:00:00",
    "gives_access": true,
    "status": "active",
    "environment": "production",
    "auto_renewal_status": "will_renew"
  },
  "payment": {
    "object": "external_subscription_payment",
    "source_subscription_identifier": "paddle_sub_id1234",
    "payment_identifier": "payment_id1234",
    "processed_at": "2023-04-01T00:00:00",
    "amount_in_local_currency": {
      "gross": 9.99,
      "currency": "USD"
    }
  }
}

Renewal

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-05-01T00:00:00",
    "current_period_starts_at": "2023-05-01T00:00:00",
    "current_period_ends_at": "2023-06-01T00:00:00",
    "gives_access": true,
    "status": "active",
    "environment": "production",
    "auto_renewal_status": "will_renew"
  },
  "payment": {
    "object": "external_subscription_payment",
    "source_subscription_identifier": "paddle_sub_id1234",
    "payment_identifier": "payment_id2345",
    "processed_at": "2023-05-01T00:00:00",
    "amount_in_local_currency": {
      "gross": 9.99,
      "currency": "USD"
    }
  }
}

Billing issue

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-06-01T00:00:00",
    "current_period_starts_at": "2023-06-01T00:00:00",
    "current_period_ends_at": "2023-06-14T00:00:00",
    "gives_access": true,
    "status": "in_grace_period",
    "environment": "production",
    "auto_renewal_status": "will_not_renew"
  },
  "payment": null
}

Billing succeeds

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-06-12T00:00:00",
    "current_period_starts_at": "2023-06-01T00:00:00",
    "current_period_ends_at": "2023-07-01T00:00:00",
    "gives_access": true,
    "status": "active",
    "environment": "production",
    "auto_renewal_status": "will_renew"
  },
  "payment": {
    "object": "external_subscription_payment",
    "source_subscription_identifier": "paddle_sub_id3456",
    "payment_identifier": "payment_id1234",
    "processed_at": "2023-06-12T00:00:00",
    "amount_in_local_currency": {
      "gross": 9.99,
      "currency": "USD"
    }
  }
}

Cancellation

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-06-18T00:00:00",
    "current_period_starts_at": "2023-06-01T00:00:00",
    "current_period_ends_at": "2023-07-01T00:00:00",
    "gives_access": true,
    "status": "active",
    "environment": "production",
    "auto_renewal_status": "will_not_renew"
  },
  "payment": null
}

Expiration

{
  "purchase": {
    "object": "external_subscription",
    "customer_id": "app_user_id12341234",
    "source_subscription_identifier": "paddle_sub_id1234",
    "source_product_identifier": "paddle_product_id1234",
    "updated_at": "2023-07-01T00:00:00",
    "current_period_starts_at": "2023-06-01T00:00:00",
    "current_period_ends_at": "2023-07-01T00:00:00",
    "gives_access": false,
    "status": "expired",
    "environment": "production",
    "auto_renewal_status": "will_not_renew"
  },
  "payment": null
}

Note: Make sure to include the current_period_starts_at for the transaction you are trying to expire. Otherwise your target transaction might not be found by the system.

Webhook events flow

Backfill Logic

Backfill new transaction in the subscription

If subscription statuses arrive out of order (eg. RevenueCat receives a previous subscription period before a subsequent subscription period), RevenueCat needs to make a decision about how the history of the subscription looked like. This section describes various scenarios that can occur.

Context

We know that the new transaction ends before the latest old one.

Overall Scenario

The transaction can fit everywhere between or overlap the existing transactions. The new transaction can be in any of the following positions.

We need to find the previous and next overlapping transactions if any.

Scenario 1

Both previous and next overlapping old transactions exist. As a result, we shrink the previous old transaction to end right before the new one. Similarly, we shrink the new transaction to end right before the next old one.

Scenario 2

Only the previous overlapping old transaction exist. As a result, we shrink the prev transaction to end right before the new one. The new transaction is not changed.

Scenario 3

Only the next overlapping old transaction exist. As a result, we shrink the new transaction to end right before the next old one.

Scenario 4

There’s no old transaction overlapping with the new one. As a result, we just store the new transaction as is.

Scenario 5

The new transaction overlaps completely a old one (prev). As a result, we send ERROR, the new transaction is skipped.

Move back the latest_expiring_transaction start date if needed

Context

We know that the new transaction ends after the latest old one.

Scenario 1

Old transaction starts before and ends during new one. As a result, we shrink the old transaction to end right before the new one

Scenario 2

Old transaction starts and ends during new one. As a result, we send ERROR, the new transaction is skipped.

Scenario 3

Old transaction starts and ends before the new one. As a result, the new transaction is saved, the old one is not modified.

Beta Limitations

The following limitations apply during Beta period:

  1. For purchase JSON with external_subscription object:
    1. Currently you cannot change the customer_id field, meaning that we don't support Transferring purchases seen on multiple App User IDs
    2. We don't generate a new event in a scenario, when current_period_ends_at was in the future, and it's changed to a different future date.
    3. We support the following options for the auto_renew_status field: will_renew, will_not_renew, unknown value. We omit the will_change_product for now, meaning that we don't support Upgrades, Downgrade, Crossgrades and resulting event (PRODUCT_CHANGE event).
    4. Family shared subscriptions are not supported (but you can still create shared subscriptions as regular subscriptions).
  2. In a scenario when payments are posted after a subscription or purchase was created we don't generate Events (since the revenue information was already sent with the INITIAL_PURCHASE / RENEWAL event)
  3. Receipt validation:
    1. Currently, we do not validate receipts or data posted to RevenueCat via the External purchases API. Customers must ensure that data posted to the endpoint is already validated, and they need to ensure that the External API Key is not getting exposed to the public.
  4. Refunds:
    1. In the scenario when the third party payment service provider retains commissions in case of a refund, Revenuecat does not track negative revenues. Meaning, that in such cases, you will see data discrepancies with the equivalent of the commission retained by the payment service provider.
  5. Entitlement identifier:
    1. We support only new entitlement identifiers (not the legacy version).
  6. Backfills that modify current_period_ends_at date, do not alter existing Events, which means that the Customer history won't reflect accurately the most up to date expiration_at_ms.
  7. If a different source_subscription_identifier gets provided, doesn’t matter if it’s the same product, we can end up with concurrent transactions
  8. EXPIRATION Events are not fired automatically. The developer needs to explicitly send a request to the External Purchases API endpoint when status changed to expired.

External Purchases

Operations to track external subscriptions and purchases.

Tracks the status of external purchases

Authorizations:
api_key
Request Body schema: application/json
required
ExternalPurchase (object) or ExternalSubscription (object) (Purchase)

The current status of one external purchase to track

Any of
object
required
any (Object)

The type of the object

Value: "external_purchase"
customer_id
required
string (Customer Id)

The app_user_id of the customer that this subscription should be associated with.

source_purchase_identifier
required
string (Source Purchase Identifier)

The purchase identifier in the external source

source_product_identifier
required
string (Source Product Identifier)

The product identifier in the external source

updated_at
required
string <date-time> (Updated At)

The date and time as of which this status was up-to-date, in ms since epoch. This is used to ensure status updates arriving out of order are not overwriting a more recent status.

Status (string) (Status)
Default: "owned"

The status of the external subscription. If not present or null, will default to unknown.

Environment (string) (Environment)
Default: "production"

The environment in which this subscription was created. If not present or null, will default to production.

metadata
object (Metadata)

Metadata about the purchase. Content must be key-value pairs with string, number, boolean, or null values.

ExternalSubscriptionPayment (object) or ExternalPurchasePayment (object) (Payment)

The current status of one external payment to track

Any of
object
required
any (Object)

The type of the object

Value: "external_subscription_payment"
source_subscription_identifier
required
string (Source Subscription Identifier)

Identifier of the subscription in the source system. This will be used to match the payment to the subscription.

payment_identifier
required
string (Payment Identifier)

A unique identifier to identify this payment. This is used to ensure that payments aren't tracked multiple times in case they get posted multiple times. You do not have to deduplicate payments. Refunds should have distinct identifiers from their original payment. If the source doesn't have a unique identifier for a payment, it is probably possible to create one from a subscription identifier plus subscription period identifier, or subscription identifier plus timestamp of the payment.

Processed At (string) (Processed At)

The date and time the payment was processed.

Country (string) (Country)

The billing country, in ISO3166.1 format.

object (AmountInLocalCurrency)

Amount of the payment or refund in the currency it was charged in. Will be converted to USD by RevenueCat unless the field amount_in_usd is present.

AmountInUSD (object)

Amount of the payment or refund in USD, if available from the transaction source.

Active Offer Type (string) (Active Offer Type)

The type of offer that was active when the payment was processed.

active_offer
string (Active Offer Identifier)

The identifier of the offer that was active when the payment was processed.

Responses

Request samples

Content type
application/json
{
  • "purchase": {
    },
  • "payment": {
    }
}

Response samples

Content type
application/json
{
  • "purchase": "string",
  • "payment": "string"
}