---
title: "Which endpoints do the RevenueCat SDKs call on RevenueDot?"
description: "Every SDK endpoint RevenueDot serves, with auth, request fields, error codes and real example responses, plus the store notification endpoints."
url: https://revenuedot.app/docs/api/sdk-endpoints
---

# Which endpoints do the RevenueCat SDKs call on RevenueDot?

The RevenueCat SDKs call these endpoints when their proxy URL points at RevenueDot. They take a **public app key** (`appl_`, `goog_`, `test_` ...), which is safe to ship in an app.
Several also accept a **secret key** for server-side use. Errors use the SDK's error format `{ "code": 7103, "message": "..." }`; see [Errors](https://revenuedot.app/docs/api/errors.md#sdk-and-rest-v1-error-codes).
Responses under `/v1` and `/rcbilling` are signed when the server has a signing key; see [Trusted Entitlements](https://revenuedot.app/docs/guides/trusted-entitlements.md).

Base URL: your server, for example `http://localhost:8787` or `https://revenuedot.example.com`. The examples read `REVENUEDOT_URL`, `PUBLIC_KEY`, `SECRET_KEY` and `PROJECT_ID` from your shell.

## Operations on this page (44)

- **Server**: [Server name and docs link](#server-name-and-docs-link), [Health check](#health-check), [Connectivity probe](#connectivity-probe)
- **Customer info**: [Get customer info](#get-customer-info)
- **Receipts**: [Post a purchase or restore](#post-a-purchase-or-restore)
- **Offerings (SDK)**: [Get offerings](#get-offerings), [Get offerings without a user](#get-offerings-without-a-user), [Test Store product details](#test-store-product-details)
- **Identity**: [Log in (identify)](#log-in-identify), [Alias two app user ids](#alias-two-app-user-ids)
- **Attributes**: [Set customer attributes](#set-customer-attributes)
- **SDK support**: [Intro offer eligibility (StoreKit 1)](#intro-offer-eligibility-storekit-1), [Sign a promotional offer (iOS)](#sign-a-promotional-offer-ios), [Attribution data (deprecated iOS call)](#attribution-data-deprecated-ios-call), [Apple AdServices token](#apple-adservices-token), [SDK health report availability](#sdk-health-report-availability), [SDK health report](#sdk-health-report), [Product to entitlement mapping (offline entitlements)](#product-to-entitlement-mapping-offline-entitlements), [Customer Center configuration (not built)](#customer-center-configuration-not-built), [Customer Center support ticket (not built)](#customer-center-support-ticket-not-built), [Virtual currency balances (not built)](#virtual-currency-balances-not-built), [Redeem a web purchase (not available)](#redeem-a-web-purchase-not-available), [Register an Apple external purchase token (iOS)](#register-an-apple-external-purchase-token-ios), [Rewarded ad verification (not available)](#rewarded-ad-verification-not-available), [Amazon receipt details (not supported)](#amazon-receipt-details-not-supported), [Paywall workflows (web SDK)](#paywall-workflows-web-sdk), [One paywall workflow (web SDK)](#one-paywall-workflow-web-sdk), [Restore eligibility (StoreKit 2)](#restore-eligibility-storekit-2), [Remote config (none yet)](#remote-config-none-yet), [Remote config (none yet)](#remote-config-none-yet), [SDK paywall and feature events (accepted, not stored)](#sdk-paywall-and-feature-events-accepted-not-stored), [SDK diagnostics (accepted, not stored)](#sdk-diagnostics-accepted-not-stored)
- **Web Billing**: [Web offering products](#web-offering-products), [Start a hosted web checkout (not available)](#start-a-hosted-web-checkout-not-available), [Web Billing purchase (not available)](#web-billing-purchase-not-available), [Prepare a Web Billing checkout (not available)](#prepare-a-web-billing-checkout-not-available), [Start a Web Billing checkout (not available)](#start-a-web-billing-checkout-not-available), [Web Billing checkout status](#web-billing-checkout-status), [Refresh Web Billing checkout pricing](#refresh-web-billing-checkout-pricing), [Complete a Web Billing checkout](#complete-a-web-billing-checkout), [Web checkout branding](#web-checkout-branding)
- **Store notifications**: [App Store Server Notifications v2](#app-store-server-notifications-v2), [Google Play real-time developer notifications (Pub/Sub push)](#google-play-real-time-developer-notifications-pubsub-push)
- **Response signing**: [Public key for response signatures](#public-key-for-response-signatures)

## Server

Health and server info. No API key.

### Server name and docs link

`GET /` · Auth: none

Answers a small JSON document. The Docker health check calls it.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/"
```

**Responses**

- **200**: Server info.

Example 200 response:

```json
{
  "name": "RevenueDot",
  "docs": "https://revenuedot.app/docs"
}
```

### Health check

`GET /v1/health` · Auth: none

Needs no API key. Use it for load balancer and uptime checks.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/health"
```

**Responses**

- **200**: The server is up.

Example 200 response:

```json
{
  "status": "ok"
}
```

### Connectivity probe

`GET /v1/health/connectivity` · Auth: none

Needs no API key. The iOS SDK probes it only with its internal API failover setting, which is off by default.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/health/connectivity"
```

**Responses**

- **200**: The server is up.

Example 200 response:

```json
{
  "status": "ok"
}
```

## Customer info

The customer's entitlements, subscriptions and one-time purchases, as the SDK decodes them into `CustomerInfo`.

### Get customer info

`GET /v1/subscribers/{app_user_id}` · Auth: public app key or secret key

What `Purchases.getCustomerInfo()` calls. Creates the customer when the app user id is new (answer 201). With a secret key the answer also has `subscriber_attributes`.
Entitlements are listed even after they expire; an entitlement is active while `expires_date` is null or in the future.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `X-Nonce` | string | no | Base64 nonce the SDK sends when entitlement verification is on; it is part of the signed message. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Customer info. Returns [CustomerInfo](#customerinfo).
- **201**: Customer info of a customer created by this call. Returns [CustomerInfo](#customerinfo).
- **400**: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "request_date": "2026-09-30T20:41:54Z",
  "request_date_ms": 1790800914034,
  "subscriber": {
    "entitlements": {
      "pro": {
        "expires_date": "2026-10-30T20:41:54Z",
        "grace_period_expires_date": null,
        "product_identifier": "pro_monthly",
        "purchase_date": "2026-09-30T20:41:54Z"
      }
    },
    "first_seen": "2026-09-30T20:41:54Z",
    "last_seen": "2026-09-30T20:41:54Z",
    "management_url": null,
    "non_subscriptions": {},
    "original_app_user_id": "user_1",
    "original_application_version": null,
    "original_purchase_date": "2026-09-30T20:41:54Z",
    "other_purchases": {},
    "subscriptions": {
      "pro_monthly": {
        "auto_resume_date": null,
        "billing_issues_detected_at": null,
        "display_name": null,
        "expires_date": "2026-10-30T20:41:54Z",
        "grace_period_expires_date": null,
        "is_sandbox": true,
        "management_url": null,
        "original_purchase_date": "2026-09-30T20:41:54Z",
        "ownership_type": "PURCHASED",
        "period_type": "normal",
        "purchase_date": "2026-09-30T20:41:54Z",
        "refunded_at": null,
        "store": "test_store",
        "store_transaction_id": "test_1790800914000_quickstart",
        "unsubscribe_detected_at": null,
        "price": {
          "amount": 9.99,
          "currency": "USD"
        }
      }
    }
  }
}
```

## Receipts

Purchases, restores and syncs. RevenueDot verifies them with the store.

### Post a purchase or restore

`POST /v1/receipts` · Auth: public app key or secret key

Every purchase, restore and `syncPurchases()` ends here. RevenueDot verifies the purchase with the store, saves it, records events and answers the updated customer info.

- **App Store:** `fetch_token` is a StoreKit 2 signed transaction (JWS), a StoreKit 1 app receipt (base64) or an Xcode StoreKit test receipt. With the app's in-app purchase key, Apple's App Store Server API supplies the full history and renewal state.
- **Google Play:** `fetch_token` is the purchase token. RevenueDot checks it with the Play Developer API and acknowledges it.
- **Test Store:** `fetch_token` is `test_<purchase time in ms>_<id>`. Any such token is accepted.

**4xx or 5xx matters.** A 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. RevenueDot answers 5xx for its own and the store's temporary failures so the SDK keeps the purchase and retries.
With a secret key, send `X-Platform` so RevenueDot knows which app the receipt belongs to.

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `X-Platform` | string | no | SDK platform (ios, android, macos, web ...). With a secret key it picks the project's app for that platform. |
| `X-Nonce` | string | no | Base64 nonce the SDK sends when entitlement verification is on; it is part of the signed message. |
| `X-Is-Sandbox` | string | no | `true` when the SDK knows the purchase is sandbox (used for StoreKit 1 receipts without an environment). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | The customer posting the receipt. |
| `fetch_token` | string | no | Receipt, signed transaction, purchase token or Test Store token. |
| `app_transaction` | string | no | StoreKit 2 app transaction JWS (accepted; not required). |
| `transaction_id` | string | no | Store transaction id. |
| `product_id` | string | no | Product being bought. |
| `product_ids` | array of string | no |  |
| `platform_product_ids` | array of object | no |  |
| `platform_product_ids[].product_id` | string | no |  |
| `platform_product_ids[].base_plan_id` | string | no |  |
| `platform_product_ids[].offer_id` | string | no |  |
| `price` | number | no |  |
| `currency` | string | no |  |
| `store_country` | string | no |  |
| `normal_duration` | string | no | ISO 8601 period of the product. |
| `is_restore` | boolean | no |  |
| `store_user_id` | string | no |  |
| `presented_offering_identifier` | string | no | Offering the purchase was made from; it appears in webhooks. |
| `attributes` | object | no | Customer attributes to save with the purchase. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/receipts" -H "Authorization: Bearer $PUBLIC_KEY" \
  -H "Content-Type: application/json" -d '{"app_user_id":"user_1","fetch_token":"test_1790800914000_quickstart","product_id":"pro_monthly","price":9.99,"currency":"USD","presented_offering_identifier":"default"}'
```

**Responses**

- **200**: Updated customer info, plus `purchased_products`. Returns [ReceiptResponse](#receiptresponse).
- **400**: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).
- **500**: Server error. The SDK keeps the purchase and retries. Returns [V1Error](#v1error).
- **503**: The store could not be reached. Retry later. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "request_date": "2026-09-30T20:41:54Z",
  "request_date_ms": 1790800914034,
  "subscriber": {
    "entitlements": {
      "pro": {
        "expires_date": "2026-10-30T20:41:54Z",
        "grace_period_expires_date": null,
        "product_identifier": "pro_monthly",
        "purchase_date": "2026-09-30T20:41:54Z"
      }
    },
    "first_seen": "2026-09-30T20:41:54Z",
    "last_seen": "2026-09-30T20:41:54Z",
    "management_url": null,
    "non_subscriptions": {},
    "original_app_user_id": "user_1",
    "original_application_version": null,
    "original_purchase_date": "2026-09-30T20:41:54Z",
    "other_purchases": {},
    "subscriptions": {
      "pro_monthly": {
        "auto_resume_date": null,
        "billing_issues_detected_at": null,
        "display_name": null,
        "expires_date": "2026-10-30T20:41:54Z",
        "grace_period_expires_date": null,
        "is_sandbox": true,
        "management_url": null,
        "original_purchase_date": "2026-09-30T20:41:54Z",
        "ownership_type": "PURCHASED",
        "period_type": "normal",
        "purchase_date": "2026-09-30T20:41:54Z",
        "refunded_at": null,
        "store": "test_store",
        "store_transaction_id": "test_1790800914000_quickstart",
        "unsubscribe_detected_at": null,
        "price": {
          "amount": 9.99,
          "currency": "USD"
        }
      }
    }
  },
  "purchased_products": {
    "pro_monthly": {
      "should_consume": false
    }
  }
}
```

## Offerings (SDK)

What the paywall shows.

### Get offerings

`GET /v1/subscribers/{app_user_id}/offerings` · Auth: public app key or secret key

What `Purchases.getOfferings()` calls. Lists active offerings with the packages whose product belongs to the calling app. `current_offering_id` is the customer's override when one is set.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/offerings" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Offerings. Returns [Offerings](#offerings).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "current_offering_id": "default",
  "offerings": [
    {
      "description": "Standard plans",
      "identifier": "default",
      "metadata": null,
      "packages": [
        {
          "identifier": "$rc_monthly",
          "platform_product_identifier": "pro_monthly"
        },
        {
          "identifier": "$rc_annual",
          "platform_product_identifier": "pro_annual"
        },
        {
          "identifier": "$rc_lifetime",
          "platform_product_identifier": "pro_lifetime"
        }
      ]
    }
  ]
}
```

### Get offerings without a user

`GET /v1/offerings` · Auth: public app key or secret key

Same answer as the per-user call, without a customer override.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/offerings" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Offerings. Returns [Offerings](#offerings).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Test Store product details

`GET /rcbilling/v1/subscribers/{app_user_id}/products` · Auth: public app key

Product details the SDK needs for Test Store (and web) products, in the web billing products shape. Prices are 0 until the catalog stores Test Store prices.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | array of string | no | Product ids; repeat the parameter. None lists every product of the app. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/rcbilling/v1/subscribers/user_1/products" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Product details.
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "product_details": [
    {
      "identifier": "pro_monthly",
      "product_type": "subscription",
      "title": "Pro monthly",
      "description": null,
      "current_price": {
        "amount": 0,
        "amount_micros": 0,
        "currency": "USD"
      },
      "normal_period_duration": "P1M",
      "default_purchase_option_id": "base",
      "default_subscription_option_id": "base",
      "purchase_options": {
        "base": {
          "id": "base",
          "price_id": "base",
          "base": {
            "period_duration": "P1M",
            "cycle_count": 1,
            "price": {
              "amount": 0,
              "amount_micros": 0,
              "currency": "USD"
            }
          },
          "base_price": null,
          "trial": null,
          "intro_price": null
        }
      },
      "subscription_options": {
        "base": {
          "id": "base",
          "price_id": "base",
          "base": {
            "period_duration": "P1M",
            "cycle_count": 1,
            "price": {
              "amount": 0,
              "amount_micros": 0,
              "currency": "USD"
            }
          },
          "base_price": null,
          "trial": null,
          "intro_price": null
        }
      }
    }
  ]
}
```

## Identity

`logIn` and aliases.

### Log in (identify)

`POST /v1/subscribers/identify` · Auth: public app key or secret key

What `Purchases.logIn()` calls. When `new_app_user_id` is new and the current id is anonymous with no other ids, the anonymous customer takes the new id (201).
When `new_app_user_id` exists, an anonymous-only current customer is merged into it (200). See [Customers and app user IDs](https://revenuedot.app/docs/concepts/customers-and-app-user-ids.md).

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | The current app user id. |
| `new_app_user_id` | string | yes | Your user id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/identify" -H "Authorization: Bearer $PUBLIC_KEY" \
  -H "Content-Type: application/json" -d '{"app_user_id":"$RCAnonymousID:abc123","new_app_user_id":"user_2"}'
```

**Responses**

- **200**: The user existed. Returns [CustomerInfo](#customerinfo).
- **201**: The user is new. Returns [CustomerInfo](#customerinfo).
- **400**: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "request_date": "2026-09-30T20:41:54Z",
  "request_date_ms": 1790800914034,
  "subscriber": {
    "entitlements": {
      "pro": {
        "expires_date": "2026-10-30T20:41:54Z",
        "grace_period_expires_date": null,
        "product_identifier": "pro_monthly",
        "purchase_date": "2026-09-30T20:41:54Z"
      }
    },
    "first_seen": "2026-09-30T20:41:54Z",
    "last_seen": "2026-09-30T20:41:54Z",
    "management_url": null,
    "non_subscriptions": {},
    "original_app_user_id": "user_1",
    "original_application_version": null,
    "original_purchase_date": "2026-09-30T20:41:54Z",
    "other_purchases": {},
    "subscriptions": {
      "pro_monthly": {
        "auto_resume_date": null,
        "billing_issues_detected_at": null,
        "display_name": null,
        "expires_date": "2026-10-30T20:41:54Z",
        "grace_period_expires_date": null,
        "is_sandbox": true,
        "management_url": null,
        "original_purchase_date": "2026-09-30T20:41:54Z",
        "ownership_type": "PURCHASED",
        "period_type": "normal",
        "purchase_date": "2026-09-30T20:41:54Z",
        "refunded_at": null,
        "store": "test_store",
        "store_transaction_id": "test_1790800914000_quickstart",
        "unsubscribe_detected_at": null,
        "price": {
          "amount": 9.99,
          "currency": "USD"
        }
      }
    }
  }
}
```

### Alias two app user ids

`POST /v1/subscribers/{app_user_id}/alias` · Auth: public app key or secret key

Links `new_app_user_id` to the customer with the same merge rules as log in. The Android SDK uses it for Block Store recovery.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `new_app_user_id` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/alias" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Accepted.
- **400**: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{}
```

## Attributes

Customer attributes such as `$email`.

### Set customer attributes

`POST /v1/subscribers/{app_user_id}/attributes` · Auth: public app key or secret key

Saves attributes such as `$email`, `$displayName` or your own keys. A null value deletes the attribute. An invalid `$email` is refused with 7263; the other attributes are saved. `collectDeviceIdentifiers()` sends `$ip` and `$deviceVersion` as `"true"`: RevenueDot stores the request's IP address and the device and OS from the SDK's headers instead.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `attributes` | object | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/attributes" -H "Authorization: Bearer $PUBLIC_KEY" \
  -H "Content-Type: application/json" -d '{"attributes":{"$email":{"value":"ana@example.com","updated_at_ms":1790800914000}}}'
```

**Responses**

- **200**: Saved.
- **400**: Some attributes were not saved. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{}
```

## SDK support

Endpoints the SDK calls for features RevenueDot answers minimally, so the SDK keeps working.

### Intro offer eligibility (StoreKit 1)

`POST /v1/subscribers/{app_user_id}/intro_eligibility` · Auth: public app key or secret key

Answers `null` (unknown) for every product, so the SDK decides eligibility on the device.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `product_identifiers` | array of string | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/intro_eligibility" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Eligibility per product.

Example 200 response:

```json
{
  "pro_monthly": null
}
```

### Sign a promotional offer (iOS)

`POST /v1/offers` · Auth: public app key or secret key

What `Purchases.promotionalOffer(forProductDiscount:product:)` calls. RevenueDot signs each offer with the App Store app's In-App Purchase key (`key_id`, `issuer_id` and `private_key` in the app's credentials) the way Apple verifies it: ECDSA P-256 with SHA-256 over the bundle id, key id, product id, offer id, app account token, nonce and timestamp, DER-encoded and base64 ([Apple's format](https://developer.apple.com/documentation/storekit/generating-a-signature-for-promotional-offers)).
The app account token matches what the SDK puts on the payment: with StoreKit 2 the lowercase app user id when it is a UUID and empty otherwise; with StoreKit 1 the app user id.
Without an In-App Purchase key the answer is 400 with code 7234, which the SDK reports as `invalidAppleSubscriptionKeyError` for that offer only.

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes |  |
| `fetch_token` | string | no | The receipt or signed transaction; not needed for signing. |
| `generate_offers` | array of object | yes |  |
| `generate_offers[].offer_id` | string | yes | Promotional offer id from App Store Connect. |
| `generate_offers[].product_id` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/offers" -H "Authorization: Bearer $PUBLIC_KEY" \
  -H "Content-Type: application/json" -d '{"app_user_id":"user_1","fetch_token":"…","generate_offers":[{"offer_id":"winback_50","product_id":"pro_monthly"}]}'
```

**Responses**

- **200**: One signature per offer.
- **400**: No In-App Purchase key, or no offers. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "offers": [
    {
      "key_id": "2X9R4HXF34",
      "offer_id": "winback_50",
      "product_id": "pro_monthly",
      "signature_data": {
        "nonce": "0f3c2a8e-5d7b-4d7e-9a53-3b8f2c1e6d40",
        "signature": "MEUCIQDD…",
        "timestamp": 1790800914034
      }
    }
  ]
}
```

### Attribution data (deprecated iOS call)

`POST /v1/subscribers/{app_user_id}/attribution` · Auth: public app key or secret key

What the deprecated `Purchases.addAttributionData` calls. The advertising identifiers in `data` (`rc_idfa`, `rc_idfv`, `rc_gps_adid`, `rc_ip_address`) become `$idfa`, `$idfv`, `$gpsAdId` and `$ip`.
For Apple Search Ads (`network` 0) with `iad-attribution` true, the iAd fields become `$mediaSource` ("Apple Search Ads"), `$campaign`, `$adGroup`, `$keyword`, `$creative` and the `$appleAds*` ids.
Attribution is write-once: a campaign attribute the customer already has is kept.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `network` | integer | yes | The SDK's AttributionNetwork: 0 Apple Search Ads. |
| `data` | object | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/attribution" -H "Authorization: Bearer $PUBLIC_KEY" \
  -H "Content-Type: application/json" -d '{"network":0,"data":{"rc_idfv":"4CEE1BEE-3C19-4591-9E34-1AD968D7B609","Version3.1":{"iad-attribution":"true","iad-campaign-name":"Spring","iad-keyword":"scanner"}}}'
```

**Responses**

- **200**: Stored.
- **400**: Bad request. For receipts, a 4xx tells the SDK the purchase can never be accepted, so it finishes the transaction. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{}
```

### Apple AdServices token

`POST /v1/subscribers/{app_user_id}/adservices_attribution` · Auth: public app key or secret key

What `enableAdServicesAttributionTokenCollection()` sends once per install (the same token can also arrive as `aad_attribution_token` on a receipt).
After answering, RevenueDot looks the token up with [Apple's attribution API](https://developer.apple.com/documentation/adservices/aaattribution/attributiontoken()), retrying a 404 or 5xx 3 times 5 seconds apart, and stores an attributed install as `$mediaSource` ("Apple Search Ads"), `$campaign`, `$adGroup`, `$keyword`, `$ad`, `$appleAdsCampaignId`, `$appleAdsAdGroupId`, `$appleAdsKeywordId`, `$appleAdsAdId`, `$appleAdsOrgId`, `$appleAdsCountryOrRegion`, `$claimType` and `$conversionType`. Apple returns ids, not names. They show on the customer page and in every webhook's `subscriber_attributes`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `aad_attribution_token` | string | yes | The token from AAAttribution.attributionToken(). |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/adservices_attribution" -H "Authorization: Bearer $PUBLIC_KEY" \
  -H "Content-Type: application/json" -d '{"aad_attribution_token":"wD3Ma…"}'
```

**Responses**

- **200**: Accepted; the lookup runs after the answer.
- **400**: No token. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{}
```

### SDK health report availability

`GET /v1/subscribers/{app_user_id}/health_report_availability` · Auth: none

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/health_report_availability"
```

**Responses**

- **200**: No report logs.

Example 200 response:

```json
{
  "report_logs": false
}
```

### SDK health report

`GET /v1/subscribers/{app_user_id}/health_report` · Auth: public app key

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/health_report" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Always passed.

Example 200 response:

```json
{
  "status": "passed",
  "project_id": "proj18pzzkao",
  "app_id": "appvnrm0a5h",
  "checks": []
}
```

### Product to entitlement mapping (offline entitlements)

`GET /v1/product_entitlement_mapping` · Auth: public app key or secret key

Lets the SDK grant entitlements while the server cannot be reached.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/product_entitlement_mapping" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: The mapping.

Example 200 response:

```json
{
  "product_entitlement_mapping": {
    "pro_monthly": {
      "product_identifier": "pro_monthly",
      "entitlements": [
        "pro"
      ]
    }
  }
}
```

### Customer Center configuration (not built)

`GET /v1/customercenter/{app_user_id}` · Auth: public app key

Always 404 with code 7259: the SDK returns an error and the Customer Center screen shows its error state. Customer Center configuration is planned for Tier 2.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/customercenter/user_1" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **404**: Not configured. Returns [V1Error](#v1error).

### Customer Center support ticket (not built)

`POST /v1/customercenter/support/create-ticket` · Auth: public app key

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/customercenter/support/create-ticket" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Not sent.

Example 200 response:

```json
{
  "sent": false
}
```

### Virtual currency balances (not built)

`GET /v1/subscribers/{app_user_id}/virtual_currencies` · Auth: public app key

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/virtual_currencies" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Empty balances.

Example 200 response:

```json
{
  "virtual_currencies": {}
}
```

### Redeem a web purchase (not available)

`POST /v1/subscribers/redeem_purchase` · Auth: public app key

What `Purchases.redeemWebPurchase()` calls with the `redemption_token` from a redemption deep link. RevenueDot takes no web payments, so no token is valid: 400 with code 7849, which the SDKs return as the `invalidToken` result.

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/redeem_purchase" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: Invalid token. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Register an Apple external purchase token (iOS)

`POST /v1/external_purchase_tokens` · Auth: public app key

Part of Apple's external purchase and link-out flows, before a web checkout. The token is acknowledged with an id, which is all the SDK reads; the web checkout that follows is not available (see `/rcbilling/v1/hosted-checkout`).

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes |  |
| `purchase_type` | `IN_APP`, `LINK_OUT` | yes |  |
| `token` | string | no | Apple's external purchase token, when there is one. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/external_purchase_tokens" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Registered.
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "id": "ept3b1f0c9e2d8a4f6b9c7e5d3a1b2c4d6e",
  "purchase_type": "LINK_OUT",
  "is_sandbox": true,
  "token_source": "APPLE_SDK"
}
```

### Rewarded ad verification (not available)

`GET /v1/subscribers/{app_user_id}/ads/reward_verifications/{client_transaction_id}` · Auth: public app key

What `pollRewardVerification` polls. There is no server-side ad verification, so the answer is always the final `failed`, and the SDK stops after one request.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |
| `client_transaction_id` | string | yes | From `generateRewardVerificationToken`. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/ads/reward_verifications/$CLIENT_TRANSACTION_ID" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Failed.
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "status": "failed",
  "reward": null,
  "failure_reason": "not_supported",
  "message": "Server-side reward verification is not available on RevenueDot."
}
```

### Amazon receipt details (not supported)

`GET /v1/receipts/amazon/{store_user_id}/{receipt_id}` · Auth: public app key

The Android SDK asks for it on Amazon subscription purchases. Amazon Appstore purchases are not supported: 400 with code 7662, the same answer as a receipt post for an Amazon app, which leaves the purchase unconsumed.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `store_user_id` | string | yes |  |
| `receipt_id` | string | yes | Not encoded by the SDK; may contain `/`. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/receipts/amazon/$STORE_USER_ID/$RECEIPT_ID" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: Not supported. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Paywall workflows (web SDK)

`GET /v1/subscribers/{app_user_id}/workflows` · Auth: public app key

What purchases-js `presentPaywall` asks first. There are no workflows, so the SDK uses the offering's own paywall.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `type` | string | no | `paywall`. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/workflows" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: No workflows.
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "workflows": [],
  "ui_config": {}
}
```

### One paywall workflow (web SDK)

`GET /v1/subscribers/{app_user_id}/workflows/{workflow_id}` · Auth: public app key

Never called, because the workflow list is empty.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |
| `workflow_id` | string | yes |  |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/subscribers/user_1/workflows/$WORKFLOW_ID" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **401**: Unknown API key. Returns [V1Error](#v1error).
- **404**: No such workflow. Returns [V1Error](#v1error).

### Restore eligibility (StoreKit 2)

`POST /v1/subscribers/{app_user_id}/restore/eligibility` · Auth: public app key

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/subscribers/user_1/restore/eligibility" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Always allowed.

Example 200 response:

```json
{
  "is_purchase_allowed_by_restore_behavior": true
}
```

### Remote config (none yet)

`GET /v1/config/{domain}` · Auth: public app key

Answers 204 (no config). `getOfferings` waits on this call.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `domain` | string | yes | Config domain the SDK asks for (for example `app`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v1/config/app" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **204**: No config.

### Remote config (none yet)

`POST /v1/config/{domain}` · Auth: public app key

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `domain` | string | yes | Config domain the SDK asks for (for example `app`). |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/config/app" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **204**: No config.

### SDK paywall and feature events (accepted, not stored)

`POST /v1/events` · Auth: public app key

Accepted so the SDK does not resend them forever.

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/events" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Accepted.

Example 200 response:

```json
{}
```

### SDK diagnostics (accepted, not stored)

`POST /v1/diagnostics` · Auth: public app key

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/diagnostics" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Accepted.

Example 200 response:

```json
{}
```

## Web Billing

Web checkout calls from the iOS SDK and purchases-js. RevenueDot takes no web payments, so a checkout answers an error the SDK shows as a failed purchase.

### Web offering products

`GET /rcbilling/v1/subscribers/{app_user_id}/offering_products` · Auth: public app key

Defined in the iOS SDK with no caller. There are no web offerings.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_user_id` | string | yes | App user id, URL-encoded (anonymous ids look like `$RCAnonymousID:...`). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/rcbilling/v1/subscribers/user_1/offering_products" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: No web offerings.
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "offerings": {}
}
```

### Start a hosted web checkout (not available)

`POST /rcbilling/v1/hosted-checkout` · Auth: public app key

The iOS SDK's paywall web checkout. RevenueDot takes no payments: 400 with code 7000, and the SDK returns `failed` for the checkout without retrying.

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/hosted-checkout" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: Not available. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Web Billing purchase (not available)

`POST /rcbilling/v1/purchase` · Auth: public app key

Defined in purchases-js with no caller. 400 with code 7000.

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/purchase" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: Not available. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Prepare a Web Billing checkout (not available)

`POST /rcbilling/v1/checkout/prepare` · Auth: public app key

purchases-js with an `rcb_` key. 400 with code 7000: the purchase fails with an error in the SDK's purchase screen.

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/checkout/prepare" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: Not available. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Start a Web Billing checkout (not available)

`POST /rcbilling/v1/checkout/start` · Auth: public app key

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/checkout/start" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: Not available. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Web Billing checkout status

`GET /rcbilling/v1/checkout/{operation_session_id}` · Auth: public app key

No checkout session exists: 400 with code 7877.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `operation_session_id` | string | yes |  |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/rcbilling/v1/checkout/$OPERATION_SESSION_ID" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: No such session. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Refresh Web Billing checkout pricing

`PATCH /rcbilling/v1/checkout/{operation_session_id}` · Auth: public app key

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `operation_session_id` | string | yes |  |

**Example request**

```bash
curl -s -X PATCH "$REVENUEDOT_URL/rcbilling/v1/checkout/$OPERATION_SESSION_ID" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: No such session. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Complete a Web Billing checkout

`POST /rcbilling/v1/checkout/{operation_session_id}/complete` · Auth: public app key

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `operation_session_id` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/rcbilling/v1/checkout/$OPERATION_SESSION_ID/complete" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **400**: No such session. Returns [V1Error](#v1error).
- **401**: Unknown API key. Returns [V1Error](#v1error).

### Web checkout branding

`GET /rcbilling/v1/branding` · Auth: public app key

purchases-js with an `rcb_` key loads it before a checkout: the app's name and the SDK's default look.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/rcbilling/v1/branding" -H "Authorization: Bearer $PUBLIC_KEY"
```

**Responses**

- **200**: Branding.
- **401**: Unknown API key. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "id": "appvnrm0a5h",
  "app_name": "Scanner Web",
  "app_icon": null,
  "app_icon_webp": null,
  "app_wordmark": null,
  "app_wordmark_webp": null,
  "appearance": null,
  "support_email": null,
  "gateway_tax_collection_enabled": false,
  "brand_font_config": null
}
```

## Store notifications

Where App Store Connect and Google Pub/Sub send server notifications.

### App Store Server Notifications v2

`POST /v1/notifications/apple/{app_id}` · Auth: none

Set this URL (shown on the app's page in the dashboard) as the Production and Sandbox Server URL in App Store Connect, with version 2 notifications.
RevenueDot verifies Apple's signature and the bundle id, stores the raw body, copies it to `notification_forward_url` when set, and applies it.

- **200:** handled, including notifications about purchases this server has not seen (stored; applied only with `track_new_purchases`).
- **400:** the payload cannot be verified or belongs to another app. App Store Connect shows it as failed.
- **404:** no App Store app with this id.
- **500:** RevenueDot failed; Apple retries.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_id` | string | yes | App id (app...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `signedPayload` | string | yes | Apple's signed JWS. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/apple/$APP_ID"
```

**Responses**

- **200**: Handled.
- **400**: Unverifiable.
- **404**: Unknown app.
- **500**: Failed; Apple retries.

Example 200 response:

```json
{
  "ok": true
}
```

### Google Play real-time developer notifications (Pub/Sub push)

`POST /v1/notifications/google/{app_id}` · Auth: Pub/Sub push token (optional)

Set this URL as the endpoint of a Pub/Sub **push** subscription on the topic Google Play publishes to.
When the app's `pubsub_audience` credential is set, the push must carry a Google-signed OIDC token for that audience (and for `pubsub_service_account` when set).
Each message is stored once (by message id), forwarded when `notification_forward_url` is set, and applied by reading the purchase from the Play Developer API.

- **200:** handled, a duplicate, ignored (another package, not a developer notification) or an invalid token that can never succeed.
- **400:** not a Pub/Sub push body. **401:** bad push token. **404:** no Google Play app with this id.
- **500 or 503:** a temporary failure; Pub/Sub redelivers.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_id` | string | yes | App id (app...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `message` | object | yes |  |
| `message.data` | string | no | Base64 JSON developer notification. |
| `message.messageId` | string | no |  |
| `message.publishTime` | string | no |  |
| `subscription` | string | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v1/notifications/google/$APP_ID"
```

**Responses**

- **200**: Handled.
- **400**: Not a push body. Returns [V1Error](#v1error).
- **401**: Bad push token. Returns [V1Error](#v1error).
- **404**: Unknown app. Returns [V1Error](#v1error).
- **500**: Temporary failure; Pub/Sub retries. Returns [V1Error](#v1error).
- **503**: Google's signing keys could not be loaded. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "status": "processed"
}
```

## Response signing

Trusted Entitlements: the public key responses are signed with.

### Public key for response signatures

`GET /.well-known/revenuedot-signing-key` · Auth: none

The Ed25519 root public key this server signs SDK responses with (Trusted Entitlements). Pin this key in SDK builds that verify responses.
The server signs only when `REVENUEDOT_SIGNING_KEY` is set; otherwise this answers 404. See [Trusted Entitlements](https://revenuedot.app/docs/guides/trusted-entitlements.md).

**Example request**

```bash
curl -s "$REVENUEDOT_URL/.well-known/revenuedot-signing-key"
```

**Responses**

- **200**: The key.
- **404**: Signing is off. Returns [V1Error](#v1error).

Example 200 response:

```json
{
  "algorithm": "Ed25519",
  "public_key": "ZzwPxGlon0E8ErpDh9QAH0Jh6+E6D6qufvTSetXZY9Y=",
  "encoding": "base64",
  "header": "X-Signature",
  "docs": "https://revenuedot.app/docs"
}
```

## Objects

The shapes the operations above send and return.

### CustomerInfo

| Field | Type | Required | Description |
|---|---|---|---|
| `request_date` | string | yes | Server time of the response. ISO 8601 in UTC, whole seconds. |
| `request_date_ms` | integer | yes | Server time of the response. Epoch milliseconds. |
| `subscriber` | object | yes |  |
| `subscriber.entitlements` | object | yes | Entitlements the customer has now or had, keyed by lookup key. Check `expires_date` (or use the SDK's `isActive`). |
| `subscriber.first_seen` | string | yes | When the customer was first seen. ISO 8601 in UTC, whole seconds. |
| `subscriber.last_seen` | string | yes | When the customer was last seen. ISO 8601 in UTC, whole seconds. |
| `subscriber.management_url` | string or null | no | Always null today. |
| `subscriber.non_subscriptions` | object | yes | One-time purchases by product id, oldest first. |
| `subscriber.original_app_user_id` | string | yes | The customer's first app user id. |
| `subscriber.original_application_version` | string or null | no | Always null today. |
| `subscriber.original_purchase_date` | string or null | no | Earliest purchase. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `subscriber.other_purchases` | object | no | Always empty. |
| `subscriber.subscriber_attributes` | object | no | Only in answers to secret-key requests. |
| `subscriber.subscriptions` | object | yes | Latest subscription per product id. |

### EntitlementInfo

| Field | Type | Required | Description |
|---|---|---|---|
| `expires_date` | string or null | yes | When access ends. Null for lifetime access. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `grace_period_expires_date` | string or null | yes | End of the billing grace period, when the store grants one. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `product_identifier` | string | yes | Store product id that gives this access. |
| `product_plan_identifier` | string | no | Google Play base plan id, only when there is one. |
| `purchase_date` | string | yes | Start of the current period. ISO 8601 in UTC, whole seconds. |

### NonSubscriptionInfo

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string or null | no |  |
| `id` | string | yes | RevenueDot purchase id. |
| `is_sandbox` | boolean | yes |  |
| `original_purchase_date` | string or null | no | Purchase time. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `purchase_date` | string or null | yes | Purchase time. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `store` | string | yes |  |
| `store_transaction_id` | string | yes | Store transaction id. |
| `price` | object | no |  |
| `price.amount` | number | yes | Price in the purchase currency. |
| `price.currency` | string | yes | ISO 4217 currency code. |

### Offerings

| Field | Type | Required | Description |
|---|---|---|---|
| `current_offering_id` | string or null | yes | Lookup key of the current offering, or the customer's override. |
| `offerings` | array of object | yes |  |
| `offerings[].description` | string | yes | Offering display name. |
| `offerings[].identifier` | string | yes | Offering lookup key. |
| `offerings[].metadata` | object or null | yes | Your JSON metadata. |
| `offerings[].packages` | array of object | yes |  |
| `offerings[].packages[].identifier` | string | yes | Package lookup key, for example $rc_monthly. |
| `offerings[].packages[].platform_product_identifier` | string | yes | Store product id for the calling app. |
| `offerings[].packages[].platform_product_plan_identifier` | string | no | Google Play base plan id, when the product is `subscription:base-plan`. |

### ReceiptResponse

| Field | Type | Required | Description |
|---|---|---|---|
| `request_date` | string | yes | Server time of the response. ISO 8601 in UTC, whole seconds. |
| `request_date_ms` | integer | yes | Server time of the response. Epoch milliseconds. |
| `subscriber` | object | yes |  |
| `subscriber.entitlements` | object | yes | Entitlements the customer has now or had, keyed by lookup key. Check `expires_date` (or use the SDK's `isActive`). |
| `subscriber.first_seen` | string | yes | When the customer was first seen. ISO 8601 in UTC, whole seconds. |
| `subscriber.last_seen` | string | yes | When the customer was last seen. ISO 8601 in UTC, whole seconds. |
| `subscriber.management_url` | string or null | no | Always null today. |
| `subscriber.non_subscriptions` | object | yes | One-time purchases by product id, oldest first. |
| `subscriber.original_app_user_id` | string | yes | The customer's first app user id. |
| `subscriber.original_application_version` | string or null | no | Always null today. |
| `subscriber.original_purchase_date` | string or null | no | Earliest purchase. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `subscriber.other_purchases` | object | no | Always empty. |
| `subscriber.subscriber_attributes` | object | no | Only in answers to secret-key requests. |
| `subscriber.subscriptions` | object | yes | Latest subscription per product id. |
| `purchased_products` | object | no | One entry per purchase the receipt contained. |

### SubscriptionInfo

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_resume_date` | string or null | no | When a paused Google Play subscription resumes. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `billing_issues_detected_at` | string or null | no | When the latest renewal failed. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `display_name` | string or null | no | Product display name. |
| `expires_date` | string or null | yes | End of the current period. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `grace_period_expires_date` | string or null | no | End of the billing grace period. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `is_sandbox` | boolean | yes | True for sandbox and Test Store purchases. |
| `management_url` | string or null | no | Always null today. |
| `original_purchase_date` | string or null | no | Start of the subscription. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `ownership_type` | `PURCHASED`, `FAMILY_SHARED` | no |  |
| `period_type` | `normal`, `trial`, `intro`, `promotional`, `prepaid` | yes |  |
| `purchase_date` | string or null | yes | Start of the current period. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `refunded_at` | string or null | no | When the store refunded it. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `store` | `app_store`, `mac_app_store`, `play_store`, `amazon`, `stripe`, `rc_billing`, `promotional`, `test_store`, `paddle`, `roku`, `external` | yes |  |
| `store_transaction_id` | string or null | no | Latest store transaction id (Apple), order id (Google) or Test Store token. |
| `unsubscribe_detected_at` | string or null | no | When auto-renew was turned off. ISO 8601 in UTC, whole seconds (for example 2026-10-30T20:41:54Z), or null. |
| `product_plan_identifier` | string | no | Google Play base plan id, only when there is one. |
| `price` | object | no |  |
| `price.amount` | number | yes | Price in the purchase currency. |
| `price.currency` | string | yes | ISO 4217 currency code. |

### V1Error

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | integer | yes | RevenueCat-compatible backend error code. See the error table. |
| `message` | string | yes | What went wrong. |
| `attribute_errors` | array of object | no | Only for 7263. |
| `attribute_errors[].key_name` | string | no |  |
| `attribute_errors[].message` | string | no |  |

## Related

- [API overview](https://revenuedot.app/docs/api.md)
- [Authentication](https://revenuedot.app/docs/api/authentication.md)
- [Errors](https://revenuedot.app/docs/api/errors.md)
- [OpenAPI document](https://revenuedot.app/docs/api/openapi.yaml)
