---
title: "What can I do with REST API v2?"
description: "REST API v2 on RevenueDot: projects, apps, products, entitlements, offerings, packages, customers, subscriptions, purchases, metrics and webhooks."
url: https://revenuedot.app/docs/api/rest-v2
---

# What can I do with REST API v2?

REST API v2 follows RevenueCat's v2 paths, objects, list envelope and error format, so existing scripts keep working when you change the base URL and key.
Authenticate with a **secret key** (`Authorization: Bearer sk_...`) or the dashboard session cookie. Lists return `{ "object": "list", "items": [...], "next_page": ..., "url": ... }`; follow `next_page` to page. `limit` is 1 to 100 (default 20).
RevenueDot-only endpoints are on [Extensions](https://revenuedot.app/docs/api/extensions.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 (73)

- **Projects**: [List projects](#list-projects), [Create a project](#create-a-project)
- **Apps**: [List apps](#list-apps), [Create an app](#create-an-app), [Get an app](#get-an-app), [Update an app and its store credentials](#update-an-app-and-its-store-credentials), [Delete an app](#delete-an-app), [Get an app's public SDK key](#get-an-apps-public-sdk-key)
- **Products**: [List products](#list-products), [Create a product](#create-a-product), [Get a product](#get-a-product), [Update a product](#update-a-product), [Delete a product](#delete-a-product), [Archive a product](#archive-a-product), [Unarchive a product](#unarchive-a-product)
- **Entitlements**: [List entitlements](#list-entitlements), [Create an entitlement](#create-an-entitlement), [Get an entitlement](#get-an-entitlement), [Rename an entitlement](#rename-an-entitlement), [Delete an entitlement](#delete-an-entitlement), [Archive an entitlement](#archive-an-entitlement), [Unarchive an entitlement](#unarchive-an-entitlement), [List an entitlement's products](#list-an-entitlements-products), [Attach products to an entitlement](#attach-products-to-an-entitlement), [Detach products from an entitlement](#detach-products-from-an-entitlement)
- **Offerings**: [List offerings](#list-offerings), [Create an offering](#create-an-offering), [Get an offering](#get-an-offering), [Update an offering or make it current](#update-an-offering-or-make-it-current), [Delete an offering](#delete-an-offering), [Archive an offering](#archive-an-offering), [Unarchive an offering](#unarchive-an-offering)
- **Packages**: [List an offering's packages](#list-an-offerings-packages), [Create a package](#create-a-package), [Get a package](#get-a-package), [Update a package](#update-a-package), [Delete a package](#delete-a-package), [List a package's products](#list-a-packages-products), [Attach products to a package](#attach-products-to-a-package), [Detach products from a package](#detach-products-from-a-package)
- **Customers**: [List or search customers](#list-or-search-customers), [Create a customer](#create-a-customer), [Get a customer](#get-a-customer), [Delete a customer](#delete-a-customer), [List a customer's app user ids](#list-a-customers-app-user-ids), [List a customer's attributes](#list-a-customers-attributes), [Set a customer's attributes](#set-a-customers-attributes), [List a customer's active entitlements](#list-a-customers-active-entitlements), [List a customer's subscriptions](#list-a-customers-subscriptions), [List a customer's one-time purchases](#list-a-customers-one-time-purchases), [List a customer's events](#list-a-customers-events), [Grant an entitlement](#grant-an-entitlement), [Revoke a granted entitlement](#revoke-a-granted-entitlement), [Assign an offering to a customer](#assign-an-offering-to-a-customer)
- **Subscriptions**: [Find subscriptions by store id](#find-subscriptions-by-store-id), [Get a subscription](#get-a-subscription), [List the entitlements a subscription unlocks](#list-the-entitlements-a-subscription-unlocks), [List a subscription's payments](#list-a-subscriptions-payments), [Cancel a subscription (Google Play)](#cancel-a-subscription-google-play), [Refund and revoke a subscription (Google Play)](#refund-and-revoke-a-subscription-google-play), [Extend a subscription](#extend-a-subscription), [Refund one payment of a subscription (Google Play)](#refund-one-payment-of-a-subscription-google-play)
- **Purchases**: [Find one-time purchases by store id](#find-one-time-purchases-by-store-id), [Get a one-time purchase](#get-a-one-time-purchase), [List the entitlements a purchase unlocks](#list-the-entitlements-a-purchase-unlocks), [Refund a one-time purchase (Google Play)](#refund-a-one-time-purchase-google-play)
- **Metrics**: [Overview metrics](#overview-metrics)
- **Webhook integrations**: [List webhooks](#list-webhooks), [Create a webhook](#create-a-webhook), [Get a webhook](#get-a-webhook), [Update a webhook](#update-a-webhook), [Delete a webhook](#delete-a-webhook)
- **Collaborators**: [List collaborators](#list-collaborators)

## Projects

Projects hold apps, the catalog, customers and webhooks.

### List projects

`GET /v2/projects` · Auth: secret key or dashboard session · Permissions: `project_configuration:projects:read`

A secret key sees its own project. A dashboard session sees every project the user is a member of.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Project](#project).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).

### Create a project

`POST /v2/projects` · Auth: dashboard session · Permissions: `project_configuration:projects:read_write`

Needs a dashboard session: a secret key belongs to one project and cannot create another. The caller becomes the project's admin.

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

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

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects" \
  -H "Content-Type: application/json" -d '{"name":"Scanner"}'
```

**Responses**

- **200**: The project. Returns [Project](#project).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).

## Apps

One app per store, each with its public SDK key and store credentials.

### List apps

`GET /v2/projects/{project_id}/apps` · Auth: secret key or dashboard session · Permissions: `project_configuration:apps:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [App](#app).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create an app

`POST /v2/projects/{project_id}/apps` · Auth: secret key or dashboard session · Permissions: `project_configuration:apps:read_write`

One app per store. `app_store` and `mac_app_store` need `bundle_id`; `play_store` and `amazon` need `package_name`. The app gets a public SDK key with the store's prefix.
Other fields in the store object are saved as store credentials (for example `subscription_private_key`, `subscription_key_id`, `subscription_key_issuer`, `play_service_account_credentials_json`). They are never returned.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `type` | `amazon`, `app_store`, `mac_app_store`, `play_store`, `stripe`, `rc_billing`, `roku`, `paddle`, `test_store` | yes |  |
| `app_store` | object | no | `bundle_id` plus optional credentials. |
| `mac_app_store` | object | no |  |
| `play_store` | object | no | `package_name` plus optional credentials. |
| `amazon` | object | no |  |
| `stripe` | object | no |  |
| `rc_billing` | object or null | no |  |
| `roku` | object or null | no |  |
| `paddle` | object or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"name":"Scanner (iOS)","type":"app_store","app_store":{"bundle_id":"com.example.scanner"}}'
```

**Responses**

- **201**: The app. Returns [App](#app).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "app",
  "id": "appugfw01uy",
  "name": "Scanner (iOS)",
  "created_at": 1790801342594,
  "type": "app_store",
  "project_id": "proj18pzzkao",
  "custom_url_scheme": "rc-4d13549313",
  "app_store": {
    "bundle_id": "com.example.scanner",
    "app_store_connect_api_key_configured": false,
    "subscription_key_configured": false,
    "app_store_connect_vendor_number": null
  }
}
```

### Get an app

`GET /v2/projects/{project_id}/apps/{app_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:apps:read`

**Path parameters**

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

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps/$APP_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The app. Returns [App](#app).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Update an app and its store credentials

`POST /v2/projects/{project_id}/apps/{app_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:apps:read_write`

Send only the store object of the app's own type. A field set to null removes that credential; other values replace it.
RevenueDot extensions in the store object: `notification_forward_url` (copy store notifications to another URL, for example RevenueCat during a dual run; null or "" turns it off), `track_new_purchases`, `allow_unsigned_receipts`, `xcode_certificate`, `app_apple_id`, `pubsub_audience`, `pubsub_service_account`. See [App Store setup](https://revenuedot.app/docs/guides/app-store.md) and [Google Play setup](https://revenuedot.app/docs/guides/google-play.md).

**Path parameters**

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

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

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `app_store` | object | no |  |
| `mac_app_store` | object | no |  |
| `play_store` | object | no |  |
| `amazon` | object | no |  |
| `stripe` | object | no |  |
| `rc_billing` | object | no |  |
| `roku` | object | no |  |
| `paddle` | object | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps/$APP_ID" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"app_store":{"subscription_private_key":"-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----","subscription_key_id":"ABC123DEFG","subscription_key_issuer":"57246542-96fe-1a63-e053-0824d011072a"}}'
```

**Responses**

- **200**: The app. Returns [App](#app).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Delete an app

`DELETE /v2/projects/{project_id}/apps/{app_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:apps:read_write`

Deletes the app and its products. Purchase history stays.

**Path parameters**

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

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps/$APP_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "app",
  "id": "…",
  "deleted_at": 1790801342625
}
```

### Get an app's public SDK key

`GET /v2/projects/{project_id}/apps/{app_id}/public_api_keys` · Auth: secret key or dashboard session · Permissions: `project_configuration:apps:read`

**Path parameters**

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

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps/$APP_ID/public_api_keys" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: One key. Returns a list of [PublicApiKey](#publicapikey).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "list",
  "items": [
    {
      "object": "public_api_key",
      "id": "pk_appvnrm0a5h",
      "key": "test_ea5120a23e7b9626f8eff225a627762c",
      "environment": "sandbox",
      "app_id": "appvnrm0a5h",
      "created_at": 1790800900758
    }
  ],
  "next_page": null,
  "url": "/v2/projects/proj18pzzkao/apps/appvnrm0a5h/public_api_keys"
}
```

## Products

Store products.

### List products

`GET /v2/projects/{project_id}/products` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `app_id` | string | no | Only this app's products. |
| `expand` | array of `items.app`, `items.indicative_price` | no | `items.app` embeds each product's app. `items.indicative_price` adds each product's Test Store price. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Product](#product).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create a product

`POST /v2/projects/{project_id}/products` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read_write`

`store_identifier` is the store's product id. For Google Play subscriptions use `subscriptionId:basePlanId`. Set `subscription.duration` (ISO 8601, for example P1M): the Test Store uses it as the period, and MRR uses it for every store. `test_store_price` sets what the SDK shows for a Test Store product.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `indicative_price` | no | `indicative_price` adds the Test Store price in RevenueCat's IndicativePrice shape (null for other stores and for products without a price). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `store_identifier` | string | yes |  |
| `app_id` | string | yes |  |
| `type` | `subscription`, `one_time`, `consumable`, `non_consumable`, `non_renewing_subscription` | yes |  |
| `display_name` | string or null | no |  |
| `title` | string or null | no | Alias of display_name. |
| `price_identifier` | string or null | no | Accepted and ignored. |
| `subscription` | object or null | no |  |
| `subscription.duration` | string or null | no | ISO 8601 period such as P1W, P1M, P1Y or P3D. |
| `test_store_price` | object or null | no | RevenueDot extension. The Test Store price the SDK shows for this product (Test Store products only). Null clears it. Read it back with `expand=indicative_price`. |
| `test_store_price.amount_micros` | integer | yes | Price in micros: 9.99 is 9990000. |
| `test_store_price.currency` | string | yes | ISO 4217 code such as USD or EUR. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"store_identifier":"pro_monthly","app_id":"appvnrm0a5h","type":"subscription","display_name":"Pro monthly","subscription":{"duration":"P1M"}}'
```

**Responses**

- **201**: The product. Returns [Product](#product).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "product",
  "id": "prode0zhpfisko",
  "store_identifier": "pro_monthly",
  "type": "subscription",
  "state": "active",
  "subscription": {
    "duration": "P1M",
    "grace_period_duration": null,
    "trial_duration": null
  },
  "created_at": 1790800900948,
  "app_id": "appvnrm0a5h",
  "display_name": "Pro monthly"
}
```

### Get a product

`GET /v2/projects/{project_id}/products/{product_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `product_id` | string | yes | Product id (prod...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `app`, `indicative_price` | no | `app` embeds the app. `indicative_price` adds the Test Store price in RevenueCat's IndicativePrice shape (null for other stores and for products without a price). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products/$PRODUCT_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The product. Returns [Product](#product).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "product",
  "id": "prode0zhpfisko",
  "store_identifier": "pro_monthly",
  "type": "subscription",
  "state": "active",
  "subscription": {
    "duration": "P1M",
    "grace_period_duration": null,
    "trial_duration": null
  },
  "created_at": 1790800900948,
  "app_id": "appvnrm0a5h",
  "display_name": "Pro monthly"
}
```

### Update a product

`POST /v2/projects/{project_id}/products/{product_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read_write`

RevenueDot also lets you correct `type` and `subscription.duration` (null clears it), and set or clear `test_store_price`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `product_id` | string | yes | Product id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `app`, `indicative_price` | no | `indicative_price` adds the Test Store price in RevenueCat's IndicativePrice shape (null for other stores and for products without a price). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string | no |  |
| `type` | `subscription`, `one_time`, `consumable`, `non_consumable`, `non_renewing_subscription` | no |  |
| `subscription` | object | no |  |
| `subscription.duration` | string or null | no |  |
| `test_store_price` | object or null | no | RevenueDot extension. The Test Store price the SDK shows for this product (Test Store products only). Null clears it. Read it back with `expand=indicative_price`. |
| `test_store_price.amount_micros` | integer | yes | Price in micros: 9.99 is 9990000. |
| `test_store_price.currency` | string | yes | ISO 4217 code such as USD or EUR. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products/$PRODUCT_ID" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"display_name":"Pro (monthly)","test_store_price":{"amount_micros":9990000,"currency":"USD"}}'
```

**Responses**

- **200**: The product. Returns [Product](#product).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Delete a product

`DELETE /v2/projects/{project_id}/products/{product_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read_write`

Detaches it from entitlements and packages. Purchase history keeps the store id.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `product_id` | string | yes | Product id. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products/$PRODUCT_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "product",
  "id": "…",
  "deleted_at": 1790801342625
}
```

### Archive a product

`POST /v2/projects/{project_id}/products/{product_id}/actions/archive` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `product_id` | string | yes | Product id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products/$PRODUCT_ID/actions/archive" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The archived product. Returns [Product](#product).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Unarchive a product

`POST /v2/projects/{project_id}/products/{product_id}/actions/unarchive` · Auth: secret key or dashboard session · Permissions: `project_configuration:products:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `product_id` | string | yes | Product id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/products/$PRODUCT_ID/actions/unarchive" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The product. Returns [Product](#product).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Entitlements

The access your app checks, unlocked by products.

### List entitlements

`GET /v2/projects/{project_id}/entitlements` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `items.product` | no | `items.product` embeds the attached products. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Entitlement](#entitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create an entitlement

`POST /v2/projects/{project_id}/entitlements` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `lookup_key` | string | yes | What apps check, for example pro. |
| `display_name` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"lookup_key":"pro","display_name":"Pro access"}'
```

**Responses**

- **201**: The entitlement. Returns [Entitlement](#entitlement).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "entitlement",
  "id": "entl1v0bp6r0qs",
  "project_id": "proj18pzzkao",
  "lookup_key": "pro",
  "display_name": "Pro access",
  "created_at": 1790800901115,
  "state": "active"
}
```

### Get an entitlement

`GET /v2/projects/{project_id}/entitlements/{entitlement_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id (entl...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `product` | no | `product` embeds the attached products. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The entitlement. Returns [Entitlement](#entitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "entitlement",
  "id": "entl1v0bp6r0qs",
  "project_id": "proj18pzzkao",
  "lookup_key": "pro",
  "display_name": "Pro access",
  "created_at": 1790800901115,
  "state": "active"
}
```

### Rename an entitlement

`POST /v2/projects/{project_id}/entitlements/{entitlement_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

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

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

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The entitlement. Returns [Entitlement](#entitlement).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Delete an entitlement

`DELETE /v2/projects/{project_id}/entitlements/{entitlement_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "entitlement",
  "id": "…",
  "deleted_at": 1790801342625
}
```

### Archive an entitlement

`POST /v2/projects/{project_id}/entitlements/{entitlement_id}/actions/archive` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID/actions/archive" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The archived entitlement. Returns [Entitlement](#entitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Unarchive an entitlement

`POST /v2/projects/{project_id}/entitlements/{entitlement_id}/actions/unarchive` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID/actions/unarchive" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The entitlement. Returns [Entitlement](#entitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List an entitlement's products

`GET /v2/projects/{project_id}/entitlements/{entitlement_id}/products` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID/products" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Product](#product).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Attach products to an entitlement

`POST /v2/projects/{project_id}/entitlements/{entitlement_id}/actions/attach_products` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

Any of these products unlocks the entitlement. Every id must belong to the project, or nothing changes.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `product_ids` | array of string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID/actions/attach_products" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"product_ids":["prode0zhpfisko","prodz2c0dt6z9x"]}'
```

**Responses**

- **200**: The entitlement with its products. Returns [Entitlement](#entitlement).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Detach products from an entitlement

`POST /v2/projects/{project_id}/entitlements/{entitlement_id}/actions/detach_products` · Auth: secret key or dashboard session · Permissions: `project_configuration:entitlements:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `entitlement_id` | string | yes | Entitlement id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `product_ids` | array of string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/entitlements/$ENTITLEMENT_ID/actions/detach_products" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The entitlement with its products. Returns [Entitlement](#entitlement).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Offerings

Groups of packages the paywall shows.

### List offerings

`GET /v2/projects/{project_id}/offerings` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `items.package`, `items.package.product` | no | Embed packages, and their products. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Offering](#offering).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create an offering

`POST /v2/projects/{project_id}/offerings` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read_write`

The project's first offering becomes current.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `lookup_key` | string | yes |  |
| `display_name` | string | yes |  |
| `metadata` | object or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"lookup_key":"default","display_name":"Standard plans"}'
```

**Responses**

- **201**: The offering. Returns [Offering](#offering).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Get an offering

`GET /v2/projects/{project_id}/offerings/{offering_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id (ofrng...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `package`, `package.product` | no | Embed packages, and their products. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The offering. Returns [Offering](#offering).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Update an offering or make it current

`POST /v2/projects/{project_id}/offerings/{offering_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read_write`

`is_current: true` makes it the only current offering. An archived offering cannot be made current (422).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string | no |  |
| `is_current` | boolean | no |  |
| `metadata` | object or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"is_current":true}'
```

**Responses**

- **200**: The offering. Returns [Offering](#offering).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

### Delete an offering

`DELETE /v2/projects/{project_id}/offerings/{offering_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read_write`

Deletes its packages and clears customer overrides that point to it.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "offering",
  "id": "…",
  "deleted_at": 1790801342625
}
```

### Archive an offering

`POST /v2/projects/{project_id}/offerings/{offering_id}/actions/archive` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID/actions/archive" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The archived offering. Returns [Offering](#offering).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

### Unarchive an offering

`POST /v2/projects/{project_id}/offerings/{offering_id}/actions/unarchive` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `unarchive_referenced_entities` | boolean | no | Also unarchive the products in its packages. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID/actions/unarchive" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The offering. Returns [Offering](#offering).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Packages

One choice on the paywall, with one product per app.

### List an offering's packages

`GET /v2/projects/{project_id}/offerings/{offering_id}/packages` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read`

Ordered by position, then creation: the order the SDK shows them in.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `items.product` | no | Embed products. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID/packages" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Package](#package).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create a package

`POST /v2/projects/{project_id}/offerings/{offering_id}/packages` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read_write`

Use the standard lookup keys (`$rc_monthly`, `$rc_annual`, `$rc_weekly`, `$rc_lifetime` ...) so the SDK's convenience accessors work. Without `position`, the package goes last.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `offering_id` | string | yes | Offering id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `lookup_key` | string | yes |  |
| `display_name` | string | yes |  |
| `position` | integer | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/offerings/$OFFERING_ID/packages" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"lookup_key":"$rc_monthly","display_name":"Monthly","position":0}'
```

**Responses**

- **201**: The package. Returns [Package](#package).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Get a package

`GET /v2/projects/{project_id}/packages/{package_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `package_id` | string | yes | Package id (pkge...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `product` | no | Embed products. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/packages/$PACKAGE_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The package. Returns [Package](#package).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Update a package

`POST /v2/projects/{project_id}/packages/{package_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `package_id` | string | yes | Package id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string | no |  |
| `position` | integer | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/packages/$PACKAGE_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The package. Returns [Package](#package).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Delete a package

`DELETE /v2/projects/{project_id}/packages/{package_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `package_id` | string | yes | Package id. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/packages/$PACKAGE_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "package",
  "id": "…",
  "deleted_at": 1790801342625
}
```

### List a package's products

`GET /v2/projects/{project_id}/packages/{package_id}/products` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `package_id` | string | yes | Package id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/packages/$PACKAGE_ID/products" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [PackageProduct](#packageproduct).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Attach products to a package

`POST /v2/projects/{project_id}/packages/{package_id}/actions/attach_products` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read_write`

One product per app, so each app's SDK finds its product. Two products of the same app can share a package only with non-overlapping `eligibility_criteria` (409 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `package_id` | string | yes | Package id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `products` | array of object | yes |  |
| `products[].product_id` | string | yes |  |
| `products[].eligibility_criteria` | `all`, `google_sdk_lt_6`, `google_sdk_ge_6` | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/packages/$PACKAGE_ID/actions/attach_products" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"products":[{"product_id":"prode0zhpfisko","eligibility_criteria":"all"}]}'
```

**Responses**

- **200**: The package with its products. Returns [Package](#package).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Detach products from a package

`POST /v2/projects/{project_id}/packages/{package_id}/actions/detach_products` · Auth: secret key or dashboard session · Permissions: `project_configuration:packages:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `package_id` | string | yes | Package id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `product_ids` | array of string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/packages/$PACKAGE_ID/actions/detach_products" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The package with its products. Returns [Package](#package).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Customers

Customers, their attributes, entitlements, subscriptions, purchases and events.

### List or search customers

`GET /v2/projects/{project_id}/customers` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read`

Newest first (by first seen).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `search` | string | no | Exact match on an app user id, the `$email` attribute (any case) or a store transaction id. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Customer](#customer).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create a customer

`POST /v2/projects/{project_id}/customers` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | App user id. |
| `attributes` | array of object | no |  |
| `attributes[].name` | string | yes |  |
| `attributes[].value` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"id":"user_42","attributes":[{"name":"$email","value":"ana@example.com"}]}'
```

**Responses**

- **201**: The customer. Returns [Customer](#customer).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Get a customer

`GET /v2/projects/{project_id}/customers/{customer_id}` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `expand` | array of `attributes` | no | `attributes` embeds the customer's attributes. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The customer. Returns [Customer](#customer).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "customer",
  "id": "user_1",
  "project_id": "proj18pzzkao",
  "first_seen_at": 1790800914012,
  "last_seen_at": 1790800914034,
  "last_seen_app_version": null,
  "last_seen_country": null,
  "last_seen_platform": null,
  "last_seen_platform_version": null,
  "active_entitlements": {
    "object": "list",
    "items": [
      {
        "object": "customer.active_entitlement",
        "entitlement_id": "entl1v0bp6r0qs",
        "expires_at": 1793392914000
      }
    ],
    "next_page": null,
    "url": "/v2/projects/proj18pzzkao/customers/user_1/active_entitlements"
  },
  "experiment": null
}
```

### Delete a customer

`DELETE /v2/projects/{project_id}/customers/{customer_id}` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read_write`

Deletes aliases, attributes, subscriptions, purchases, transactions and events. Cannot be undone.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "customer",
  "id": "…",
  "deleted_at": 1790801342625
}
```

### List a customer's app user ids

`GET /v2/projects/{project_id}/customers/{customer_id}/aliases` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/aliases" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [CustomerAlias](#customeralias).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List a customer's attributes

`GET /v2/projects/{project_id}/customers/{customer_id}/attributes` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/attributes" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [CustomerAttribute](#customerattribute).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Set a customer's attributes

`POST /v2/projects/{project_id}/customers/{customer_id}/attributes` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read_write`

API writes always win over older SDK writes. A null value deletes the attribute.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `attributes` | array of object | yes |  |
| `attributes[].name` | string | yes |  |
| `attributes[].value` | string or null | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/attributes" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"attributes":[{"name":"$displayName","value":"Ana"}]}'
```

**Responses**

- **200**: Every attribute of the customer. Returns a list of [CustomerAttribute](#customerattribute).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List a customer's active entitlements

`GET /v2/projects/{project_id}/customers/{customer_id}/active_entitlements` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/active_entitlements" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [ActiveEntitlement](#activeentitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List a customer's subscriptions

`GET /v2/projects/{project_id}/customers/{customer_id}/subscriptions` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `environment` | `production`, `sandbox` | no | Only this environment. Default: both. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/subscriptions" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of subscriptions. Returns a list of [Subscription](#subscription).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "list",
  "items": [
    {
      "object": "subscription",
      "id": "sub_k1u15wepvw0dfh25",
      "customer_id": "user_1",
      "original_customer_id": "user_1",
      "product_id": "prode0zhpfisko",
      "starts_at": 1790800914000,
      "current_period_starts_at": 1790800914000,
      "current_period_ends_at": 1793392914000,
      "ends_at": 1793392914000,
      "gives_access": true,
      "pending_payment": false,
      "auto_renewal_status": "will_renew",
      "status": "active",
      "total_revenue_in_usd": {
        "currency": "USD",
        "gross": 9.99,
        "commission": 0,
        "tax": 0,
        "proceeds": 9.99
      },
      "presented_offering_id": null,
      "entitlements": {
        "object": "list",
        "items": [
          {
            "object": "entitlement",
            "id": "entl1v0bp6r0qs",
            "project_id": "proj18pzzkao",
            "lookup_key": "pro",
            "display_name": "Pro access",
            "created_at": 1790800901115,
            "state": "active"
          }
        ],
        "next_page": null,
        "url": "/v2/projects/proj18pzzkao/subscriptions/sub_k1u15wepvw0dfh25/entitlements"
      },
      "environment": "sandbox",
      "store": "test_store",
      "store_subscription_identifier": "test_1790800914000_quickstart",
      "ownership": "purchased",
      "management_url": null
    }
  ],
  "next_page": null,
  "url": "/v2/projects/proj18pzzkao/customers/user_1/subscriptions"
}
```

### List a customer's one-time purchases

`GET /v2/projects/{project_id}/customers/{customer_id}/purchases` · Auth: secret key or dashboard session · Permissions: `customer_information:purchases:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `environment` | `production`, `sandbox` | no | Only this environment. Default: both. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/purchases" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Purchase](#purchase).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List a customer's events

`GET /v2/projects/{project_id}/customers/{customer_id}/events` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read`

Newest first. `body` is the webhook event.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `environment` | `production`, `sandbox` | no | Only this environment. Default: both. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/events" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [CustomerEvent](#customerevent).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Grant an entitlement

`POST /v2/projects/{project_id}/customers/{customer_id}/actions/grant_entitlement` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read_write`

Promotional access until `expires_at`. A grant ending within 2 hours of an existing grant for the same entitlement changes nothing.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `entitlement_id` | string | yes | Entitlement id (entl...). |
| `expires_at` | integer | yes | Epoch milliseconds, in the future. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/actions/grant_entitlement" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"entitlement_id":"entl1v0bp6r0qs","expires_at":1830000000000}'
```

**Responses**

- **201**: The customer. Returns [Customer](#customer).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Revoke a granted entitlement

`POST /v2/projects/{project_id}/customers/{customer_id}/actions/revoke_granted_entitlement` · Auth: secret key or dashboard session · Permissions: `customer_information:customers:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

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

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

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/actions/revoke_granted_entitlement" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The customer. Returns [Customer](#customer).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Assign an offering to a customer

`POST /v2/projects/{project_id}/customers/{customer_id}/actions/assign_offering` · Auth: secret key or dashboard session · Permissions: `project_configuration:offerings:read`, `customer_information:customers:read_write`

The customer sees this offering as current. `null` removes the override.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `customer_id` | string | yes | Any app user id of the customer. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `offering_id` | string or null | yes | Offering id (ofrng...) or null. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/customers/user_1/actions/assign_offering" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"offering_id":"ofrngjfr71v5awb"}'
```

**Responses**

- **200**: Done.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{}
```

## Subscriptions

Subscriptions across customers, and store actions on them.

### Find subscriptions by store id

`GET /v2/projects/{project_id}/subscriptions` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `store_subscription_identifier` | string | yes | Store transaction id, original transaction id or purchase token. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Subscription](#subscription).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Get a subscription

`GET /v2/projects/{project_id}/subscriptions/{subscription_id}` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id (sub_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The subscription. Returns [Subscription](#subscription).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "subscription",
  "id": "sub_k1u15wepvw0dfh25",
  "customer_id": "user_1",
  "original_customer_id": "user_1",
  "product_id": "prode0zhpfisko",
  "starts_at": 1790800914000,
  "current_period_starts_at": 1790800914000,
  "current_period_ends_at": 1793392914000,
  "ends_at": 1793392914000,
  "gives_access": true,
  "pending_payment": false,
  "auto_renewal_status": "will_renew",
  "status": "active",
  "total_revenue_in_usd": {
    "currency": "USD",
    "gross": 9.99,
    "commission": 0,
    "tax": 0,
    "proceeds": 9.99
  },
  "presented_offering_id": null,
  "entitlements": {
    "object": "list",
    "items": [
      {
        "object": "entitlement",
        "id": "entl1v0bp6r0qs",
        "project_id": "proj18pzzkao",
        "lookup_key": "pro",
        "display_name": "Pro access",
        "created_at": 1790800901115,
        "state": "active"
      }
    ],
    "next_page": null,
    "url": "/v2/projects/proj18pzzkao/subscriptions/sub_k1u15wepvw0dfh25/entitlements"
  },
  "environment": "sandbox",
  "store": "test_store",
  "store_subscription_identifier": "test_1790800914000_quickstart",
  "ownership": "purchased",
  "management_url": null
}
```

### List the entitlements a subscription unlocks

`GET /v2/projects/{project_id}/subscriptions/{subscription_id}/entitlements` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID/entitlements" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Entitlement](#entitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List a subscription's payments

`GET /v2/projects/{project_id}/subscriptions/{subscription_id}/transactions` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read`

One item per paid store transaction of the subscription (purchase, trial start, renewal). A refunded payment's `effective_expiration_date` is the refund time.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `sort` | `id`, `purchased_at` | no | Default id. |
| `direction` | `asc`, `desc` | no | Default asc. |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID/transactions" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [SubscriptionTransaction](#subscriptiontransaction).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Cancel a subscription (Google Play)

`POST /v2/projects/{project_id}/subscriptions/{subscription_id}/actions/cancel` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read_write`

Google Play only: turns auto-renew off. Other stores answer 422.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID/actions/cancel" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The subscription. Returns [Subscription](#subscription).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).
- **503**: The store could not be reached. Retry later. Returns [V2Error](#v2error).

### Refund and revoke a subscription (Google Play)

`POST /v2/projects/{project_id}/subscriptions/{subscription_id}/actions/refund` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read_write`

Google Play only: refunds the latest payment and ends access now. App Store refunds go through Apple. Other stores answer 422.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID/actions/refund" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The subscription. Returns [Subscription](#subscription).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).
- **503**: The store could not be reached. Retry later. Returns [V2Error](#v2error).

### Extend a subscription

`POST /v2/projects/{project_id}/subscriptions/{subscription_id}/actions/extend` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read_write`

App Store: Apple extends the renewal date (1 to 90 days, `extend_reason_code` required, needs the in-app purchase key). Google Play: the renewal is deferred (up to 365 days). Send `extend_by_days` or `extend_until_ms`, not both.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `extend_by_days` | integer | yes |  |
| `extend_reason_code` | `undeclared`, `customer_satisfaction`, `other`, `service_issue_or_outage` | no | Apple's reason for the extension. Required for App Store subscriptions. |
| `extend_until_ms` | integer | yes | New end, epoch milliseconds. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID/actions/extend" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"extend_by_days":7,"extend_reason_code":"customer_satisfaction"}'
```

**Responses**

- **200**: The subscription. Returns [Subscription](#subscription).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).
- **503**: The store could not be reached. Retry later. Returns [V2Error](#v2error).

### Refund one payment of a subscription (Google Play)

`POST /v2/projects/{project_id}/subscriptions/{subscription_id}/transactions/{transaction_id}/actions/refund` · Auth: secret key or dashboard session · Permissions: `customer_information:subscriptions:read_write`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `subscription_id` | string | yes | Subscription id. |
| `transaction_id` | string | yes | Google order id of the payment. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/subscriptions/$SUBSCRIPTION_ID/transactions/$TRANSACTION_ID/actions/refund" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The refunded payment. Returns [SubscriptionTransaction](#subscriptiontransaction).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).
- **503**: The store could not be reached. Retry later. Returns [V2Error](#v2error).

## Purchases

One-time purchases across customers.

### Find one-time purchases by store id

`GET /v2/projects/{project_id}/purchases` · Auth: secret key or dashboard session · Permissions: `customer_information:purchases:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `store_purchase_identifier` | string | yes | Store transaction id. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/purchases" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Purchase](#purchase).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Get a one-time purchase

`GET /v2/projects/{project_id}/purchases/{purchase_id}` · Auth: secret key or dashboard session · Permissions: `customer_information:purchases:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `purchase_id` | string | yes | Purchase id. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/purchases/$PURCHASE_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The purchase. Returns [Purchase](#purchase).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List the entitlements a purchase unlocks

`GET /v2/projects/{project_id}/purchases/{purchase_id}/entitlements` · Auth: secret key or dashboard session · Permissions: `customer_information:purchases:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `purchase_id` | string | yes | Purchase id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/purchases/$PURCHASE_ID/entitlements" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Entitlement](#entitlement).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Refund a one-time purchase (Google Play)

`POST /v2/projects/{project_id}/purchases/{purchase_id}/actions/refund` · Auth: secret key or dashboard session · Permissions: `customer_information:purchases:read_write`

Google Play refunds and revokes the order. Other stores answer 422.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `purchase_id` | string | yes | Purchase id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/purchases/$PURCHASE_ID/actions/refund" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The purchase. Returns [Purchase](#purchase).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).
- **503**: The store could not be reached. Retry later. Returns [V2Error](#v2error).

## Metrics

The dashboard overview numbers.

### Overview metrics

`GET /v2/projects/{project_id}/metrics/overview` · Auth: secret key or dashboard session · Permissions: `charts_metrics:overview:read`

Computed live: active trials, active paid subscriptions, MRR (USD price normalised to a month), revenue in the last 28 days, new and active customers in the last 28 days.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `currency` | `"USD"` | no | Only USD is supported. |
| `environment` | `production`, `sandbox` | no | RevenueDot extension. Default production. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/metrics/overview" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The metrics. Returns [OverviewMetrics](#overviewmetrics).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Webhook integrations

Where events are sent.

### List webhooks

`GET /v2/projects/{project_id}/integrations/webhooks` · Auth: secret key or dashboard session · Permissions: `project_configuration:integrations:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/integrations/webhooks" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [WebhookIntegration](#webhookintegration).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create a webhook

`POST /v2/projects/{project_id}/integrations/webhooks` · Auth: secret key or dashboard session · Permissions: `project_configuration:integrations:read_write`

The answer includes `signing_secret` (whsec_...) once. Store it: it verifies the `X-RevenueCat-Webhook-Signature` header. See [Webhooks](https://revenuedot.app/docs/guides/webhooks.md).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `url` | string | yes | http(s) URL. |
| `authorization_header` | string or null | no | Sent as the Authorization header. |
| `environment` | `production`, `sandbox`, null | no | Null or absent: both. |
| `event_types` | array of `initial_purchase`, `renewal`, `product_change`, `cancellation`, `billing_issue`, `non_renewing_purchase`, `uncancellation`, `transfer`, `subscription_paused`, `expiration`, `subscription_extended`, `invoice_issuance`, `temporary_entitlement_grant`, `refund_reversed`, `virtual_currency_transaction`, `test`, `experiment_enrollment`, `purchase_redeemed`, `subscriber_alias`, `price_increase_consent_required`, `price_increase_consent_approved` | no | Empty or absent: every type. |
| `app_id` | string or null | no | Only this app's events. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/integrations/webhooks" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"name":"Backend","url":"https://api.example.com/webhooks/revenuedot","authorization_header":"Bearer my-shared-token","environment":"production","event_types":["initial_purchase","renewal"]}'
```

**Responses**

- **201**: The webhook with its signing secret. Returns [WebhookIntegration](#webhookintegration).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "webhook_integration",
  "id": "wh_ceps8nr7mczvhaqw",
  "project_id": "proj18pzzkao",
  "name": "Backend",
  "url": "https://api.example.com/webhooks/revenuedot",
  "environment": "production",
  "event_types": [
    "initial_purchase",
    "renewal"
  ],
  "app_id": null,
  "created_at": 1790801342625,
  "signing_secret": "whsec_3f5b7fb5a591c17aaa9108376df0bddbe1555f0a908bfdc0"
}
```

### Get a webhook

`GET /v2/projects/{project_id}/integrations/webhooks/{webhook_integration_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:integrations:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `webhook_integration_id` | string | yes | Webhook id (wh_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/integrations/webhooks/$WEBHOOK_INTEGRATION_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: The webhook. Returns [WebhookIntegration](#webhookintegration).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Update a webhook

`POST /v2/projects/{project_id}/integrations/webhooks/{webhook_integration_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:integrations:read_write`

`enabled` is a RevenueDot extension: false pauses deliveries without deleting the webhook. Events recorded while it is off are not sent; queued retries resume when it is turned on. Read it with `GET /v2/projects/{project_id}/webhooks`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `webhook_integration_id` | string | yes | Webhook id. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `url` | string | no |  |
| `authorization_header` | string or null | no |  |
| `environment` | string or null | no |  |
| `event_types` | array of string | no |  |
| `app_id` | string or null | no |  |
| `enabled` | boolean | no | RevenueDot extension. False pauses deliveries. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/integrations/webhooks/$WEBHOOK_INTEGRATION_ID" -H "Authorization: Bearer $SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"enabled":false}'
```

**Responses**

- **200**: The webhook. Returns [WebhookIntegration](#webhookintegration).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Delete a webhook

`DELETE /v2/projects/{project_id}/integrations/webhooks/{webhook_integration_id}` · Auth: secret key or dashboard session · Permissions: `project_configuration:integrations:read_write`

Pending deliveries are deleted with it.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |
| `webhook_integration_id` | string | yes | Webhook id. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/integrations/webhooks/$WEBHOOK_INTEGRATION_ID" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: Deleted. Returns [Deleted](#deleted).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "webhook_integration",
  "id": "…",
  "deleted_at": 1790801342625
}
```

## Collaborators

Dashboard users of the project.

### List collaborators

`GET /v2/projects/{project_id}/collaborators` · Auth: secret key or dashboard session · Permissions: `project_configuration:collaborators:read`

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | Project id (proj...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/collaborators" -H "Authorization: Bearer $SECRET_KEY"
```

**Responses**

- **200**: A page of results. Returns a list of [Collaborator](#collaborator).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Objects

The shapes the operations above send and return.

### ActiveEntitlement

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"customer.active_entitlement"` | yes |  |
| `entitlement_id` | string | yes | Entitlement id (entl...), not the lookup key. |
| `expires_at` | integer or null | yes | When access ends. Epoch milliseconds, or null. |

### App

Only the object for the app's own `type` is present. Store secrets are never returned.

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"app"` | yes |  |
| `id` | string | yes | App id (app...). |
| `name` | string | yes |  |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `type` | `amazon`, `app_store`, `mac_app_store`, `play_store`, `stripe`, `rc_billing`, `roku`, `paddle`, `test_store` | yes |  |
| `project_id` | string | yes |  |
| `custom_url_scheme` | string | no | Derived from the public key. |
| `app_store` | object | no |  |
| `app_store.bundle_id` | string | no |  |
| `app_store.app_store_connect_api_key_configured` | boolean | no |  |
| `app_store.subscription_key_configured` | boolean | no | True when the in-app purchase key (.p8, key id, issuer id) is set. |
| `app_store.app_store_connect_vendor_number` | string or null | no |  |
| `mac_app_store` | object | no |  |
| `mac_app_store.bundle_id` | string | no |  |
| `play_store` | object | no |  |
| `play_store.package_name` | string | no |  |
| `play_store.play_service_account_credentials_configured` | boolean | no |  |
| `amazon` | object | no |  |
| `amazon.package_name` | string | no |  |
| `stripe` | object | no |  |
| `stripe.stripe_account_id` | string or null | no |  |
| `rc_billing` | object | no |  |
| `rc_billing.stripe_account_id` | string or null | no |  |
| `rc_billing.seller_company_name` | string | no |  |
| `rc_billing.app_name` | string | no |  |
| `rc_billing.support_email` | string or null | no |  |
| `rc_billing.default_currency` | string | no |  |
| `roku` | object | no |  |
| `roku.roku_channel_id` | string or null | no |  |
| `roku.roku_channel_name` | string or null | no |  |
| `paddle` | object | no |  |
| `paddle.paddle_is_sandbox` | boolean | no |  |
| `paddle.paddle_api_key` | null | no |  |

### Collaborator

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"collaborator"` | yes |  |
| `id` | string | yes |  |
| `name` | string or null | no |  |
| `email` | string | yes |  |
| `role` | `admin`, `developer`, `read_only` | yes | RevenueCat's role names. `read_only` is the dashboard's Viewer role. |
| `accepted_at` | integer | no | When the user joined. Epoch milliseconds. |
| `has_mfa` | boolean | no | Always false. |

### Customer

`active_entitlements` and `experiment` are present on single-customer answers; `attributes` only with `expand=attributes`.

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"customer"` | yes |  |
| `id` | string | yes | The customer's original app user id. |
| `project_id` | string | yes |  |
| `first_seen_at` | integer | yes | First seen. Epoch milliseconds. |
| `last_seen_at` | integer or null | yes | Last seen. Epoch milliseconds, or null. |
| `last_seen_app_version` | string or null | no |  |
| `last_seen_country` | string or null | no |  |
| `last_seen_platform` | string or null | no |  |
| `last_seen_platform_version` | null | no |  |
| `active_entitlements` | object | no |  |
| `active_entitlements.object` | `"list"` | yes |  |
| `active_entitlements.items` | array of ActiveEntitlement | yes |  |
| `active_entitlements.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `active_entitlements.url` | string | yes | Path of this list. |
| `experiment` | null | no |  |
| `attributes` | object | no |  |
| `attributes.object` | `"list"` | yes |  |
| `attributes.items` | array of CustomerAttribute | yes |  |
| `attributes.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `attributes.url` | string | yes | Path of this list. |

### CustomerAlias

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"customer.alias"` | yes |  |
| `id` | string | yes | An app user id of the customer. |
| `created_at` | integer | yes | When it was linked. Epoch milliseconds. |

### CustomerAttribute

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"customer.attribute"` | yes |  |
| `name` | string | yes |  |
| `value` | string | yes |  |
| `updated_at` | integer | yes | Last update. Epoch milliseconds. |

### CustomerEvent

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"customer.event"` | yes |  |
| `id` | string | yes |  |
| `app_id` | string or null | no |  |
| `type` | string | yes | Webhook event type, for example INITIAL_PURCHASE. |
| `body` | object | yes | The webhook `event` object. |
| `created_at` | integer | yes | Recorded. Epoch milliseconds. |
| `occurred_at` | integer | yes | When it happened. Epoch milliseconds. |

### Deleted

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | string | yes | The deleted object's type. |
| `id` | string | yes |  |
| `deleted_at` | integer | yes | When it was deleted. Epoch milliseconds. |

### Entitlement

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"entitlement"` | yes |  |
| `id` | string | yes | Entitlement id (entl...). |
| `project_id` | string | yes |  |
| `lookup_key` | string | yes | What apps check, for example `pro`. |
| `display_name` | string | yes |  |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `state` | `active`, `inactive` | yes |  |
| `products` | object | no |  |
| `products.object` | `"list"` | yes |  |
| `products.items` | array of Product | yes |  |
| `products.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `products.url` | string | yes | Path of this list. |

### IndicativePrice

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"indicative_price"` | yes |  |
| `currency` | string | yes | ISO 4217 code. |
| `country` | null | yes |  |
| `amount_micros` | integer | yes | Price in micros: 9.99 is 9990000. |

### MonetaryAmount

| Field | Type | Required | Description |
|---|---|---|---|
| `currency` | string | yes | ISO 4217 currency code. |
| `gross` | number | yes | Gross amount. |
| `commission` | number | yes | Estimated store commission. |
| `tax` | number | yes | Tax. Always 0 today. |
| `proceeds` | number | yes | Gross minus commission. |

### Offering

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"offering"` | yes |  |
| `id` | string | yes | Offering id (ofrng...). |
| `lookup_key` | string | yes |  |
| `display_name` | string | yes |  |
| `is_current` | boolean | yes | Exactly one offering per project is current. |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `project_id` | string | yes |  |
| `state` | `active`, `inactive` | yes |  |
| `paywall_id` | null | no |  |
| `metadata` | object or null | yes |  |
| `packages` | object | no |  |
| `packages.object` | `"list"` | yes |  |
| `packages.items` | array of Package | yes |  |
| `packages.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `packages.url` | string | yes | Path of this list. |

### OverviewMetrics

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"overview_metrics"` | yes |  |
| `currency` | `"USD"` | yes |  |
| `metrics` | array of object | yes |  |
| `metrics[].object` | `"overview_metric"` | no |  |
| `metrics[].id` | `active_trials`, `active_subscriptions`, `mrr`, `revenue`, `new_customers`, `active_users` | no |  |
| `metrics[].name` | string | no |  |
| `metrics[].description` | string | no |  |
| `metrics[].unit` | `#`, `$` | no |  |
| `metrics[].period` | `P0D`, `P28D` | no |  |
| `metrics[].value` | number | no |  |
| `metrics[].last_updated_at` | integer | no | Computed at. Epoch milliseconds. |
| `metrics[].last_updated_at_iso8601` | string | no |  |

### Package

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"package"` | yes |  |
| `id` | string | yes | Package id (pkge...). |
| `lookup_key` | string | yes | For example $rc_monthly. |
| `display_name` | string | yes |  |
| `position` | integer | yes | Order in the offering, lowest first. |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `products` | object | no |  |
| `products.object` | `"list"` | yes |  |
| `products.items` | array of PackageProduct | yes |  |
| `products.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `products.url` | string | yes | Path of this list. |

### PackageProduct

| Field | Type | Required | Description |
|---|---|---|---|
| `product` | Product | yes |  |
| `eligibility_criteria` | `all`, `google_sdk_lt_6`, `google_sdk_ge_6` | yes |  |

### Product

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"product"` | yes |  |
| `id` | string | yes | Product id (prod...). |
| `store_identifier` | string | yes | The store's product id. Google Play subscriptions use `subscriptionId:basePlanId`. |
| `type` | `subscription`, `one_time`, `consumable`, `non_consumable`, `non_renewing_subscription` | yes |  |
| `state` | `active`, `inactive` | yes |  |
| `subscription` | object | no |  |
| `subscription.duration` | string or null | no | ISO 8601 period such as P1M. |
| `subscription.grace_period_duration` | null | no |  |
| `subscription.trial_duration` | null | no |  |
| `one_time` | object | no |  |
| `one_time.is_consumable` | boolean or null | no |  |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `app_id` | string | yes |  |
| `display_name` | string or null | yes |  |
| `app` | App | no | Only the object for the app's own `type` is present. Store secrets are never returned. |
| `indicative_price` | IndicativePrice or null | no | With `expand=indicative_price`: the Test Store price, or null. |

### Project

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"project"` | yes |  |
| `id` | string | yes | Project id (proj...). |
| `name` | string | yes |  |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `icon_url` | string or null | no | Always null. |
| `icon_url_large` | string or null | no | Always null. |

### PublicApiKey

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"public_api_key"` | yes |  |
| `id` | string | yes |  |
| `key` | string | yes | The key the SDK sends (appl_, goog_, test_ ...). |
| `environment` | `production`, `sandbox` | yes |  |
| `app_id` | string | yes |  |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |

### Purchase

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"purchase"` | yes |  |
| `id` | string | yes |  |
| `customer_id` | string | yes |  |
| `original_customer_id` | string | no |  |
| `product_id` | string | yes |  |
| `purchased_at` | integer | yes | Purchase time. Epoch milliseconds. |
| `revenue_in_usd` | MonetaryAmount | no |  |
| `quantity` | integer | no |  |
| `status` | `owned`, `refunded` | yes |  |
| `presented_offering_id` | string or null | no | Offering the purchase was made from (its id, or the identifier the SDK sent when no such offering exists). |
| `entitlements` | object | no |  |
| `entitlements.object` | `"list"` | yes |  |
| `entitlements.items` | array of Entitlement | yes |  |
| `entitlements.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `entitlements.url` | string | yes | Path of this list. |
| `environment` | `production`, `sandbox` | yes |  |
| `store` | string | yes |  |
| `store_purchase_identifier` | string | no |  |
| `ownership` | `purchased` | no |  |
| `country` | string | no |  |

### Subscription

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"subscription"` | yes |  |
| `id` | string | yes | Subscription id (sub_...). |
| `customer_id` | string | yes |  |
| `original_customer_id` | string | no |  |
| `product_id` | string or null | no | Product id (prod...), null for promotional grants. |
| `starts_at` | integer | yes | Start of the subscription. Epoch milliseconds. |
| `current_period_starts_at` | integer | no | Start of the current period. Epoch milliseconds. |
| `current_period_ends_at` | integer or null | no | End of the current period. Epoch milliseconds, or null. |
| `ends_at` | integer or null | no | End of access. Epoch milliseconds, or null. |
| `gives_access` | boolean | yes |  |
| `pending_payment` | boolean | no |  |
| `auto_renewal_status` | `will_renew`, `will_not_renew`, `will_change_product`, `will_pause` | yes |  |
| `status` | `trialing`, `active`, `in_grace_period`, `in_billing_retry`, `paused`, `expired` | yes |  |
| `total_revenue_in_usd` | MonetaryAmount | no |  |
| `presented_offering_id` | string or null | no | Offering the purchase was made from (its id, or the identifier the SDK sent when no such offering exists). |
| `entitlements` | object | no |  |
| `entitlements.object` | `"list"` | yes |  |
| `entitlements.items` | array of Entitlement | yes |  |
| `entitlements.next_page` | string or null | yes | Path of the next page, or null on the last page. |
| `entitlements.url` | string | yes | Path of this list. |
| `environment` | `production`, `sandbox` | yes |  |
| `store` | string | yes |  |
| `store_subscription_identifier` | string | no | Latest store transaction id, order id or token. |
| `ownership` | `purchased`, `family_shared` | no |  |
| `country` | string | no | ISO 3166-1 alpha-2, when known. |
| `management_url` | null | no |  |

### SubscriptionTransaction

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"subscription_transaction"` | yes |  |
| `id` | string | yes |  |
| `purchased_at` | integer | yes | Purchase time. Epoch milliseconds. |
| `product_store_identifier` | string | no |  |
| `revenue_in_local_currency` | MonetaryAmount or null | no |  |
| `revenue_in_usd` | MonetaryAmount | no |  |
| `expiration_date` | integer or null | no | End of the period. Epoch milliseconds, or null. |
| `effective_expiration_date` | integer or null | no | When access actually ended (the refund time for a refunded period). Epoch milliseconds, or null. |

### V2Error

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"error"` | yes |  |
| `type` | `parameter_error`, `resource_already_exists`, `resource_missing`, `idempotency_error`, `rate_limit_error`, `authentication_error`, `authorization_error`, `store_error`, `server_error`, `resource_locked_error`, `unprocessable_entity_error`, `invalid_request`, `entity_references_archived_entities` | yes |  |
| `message` | string | yes | What went wrong. |
| `param` | string | no | The request field at fault, when there is one. |
| `doc_url` | string | yes | Link to the error's section of the errors page. |
| `retryable` | boolean | yes | True when retrying the same request can succeed. |

### WebhookIntegration

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"webhook_integration"` | yes |  |
| `id` | string | yes | Webhook id (wh_...). |
| `project_id` | string | yes |  |
| `name` | string | yes |  |
| `url` | string | yes |  |
| `environment` | `production`, `sandbox`, null | yes | Null sends both. |
| `event_types` | array of string | yes | Lower-case event types. Empty sends every type. |
| `app_id` | string or null | yes | Only events of this app, or null for all. |
| `created_at` | integer | yes | Creation time. Epoch milliseconds. |
| `signing_secret` | string | no | whsec_... Only in the answer that creates the webhook. |

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