# Superwall: Subscription Infrastructure for iOS, Android, and Web

Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.

## Pricing

- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.

Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.

## Scale

$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.

## Infrastructure capabilities

- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan

Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.

## Migration

Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).

## Paywall product (optional, separately billable)

One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.

## Architecture

Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.

## Docs

* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing

# Chart Annotations

Mark releases, campaigns, and incidents on your app's charts from the dashboard or the Superwall API.

Chart annotations mark what happened on your app's timeline, such as a release, an App Store feature, a marketing campaign, or an outage. Every time-series chart for the app draws them, so you can see a change in a metric next to the event that might explain it.

Annotations belong to the app, not to one chart or one person. Everyone who can see the app sees the same annotations.

An annotation is one of two kinds:

| Kind  | What it marks                                           | How it's drawn          |
| ----- | ------------------------------------------------------- | ----------------------- |
| Point | One moment, such as a release.                          | A dashed vertical line. |
| Range | A span of time, such as a sale or an App Store feature. | A faint shaded band.    |

Each annotation keeps the same color on every chart.

## Annotations on charts

> **Note:** Annotations are drawn in Dashboard v2. Enable **New Dashboard Experience** from
> **Settings > Public Beta**, then open your charts at `app.superwall.com`. The
> [API](#manage-annotations-with-the-api) works whichever dashboard you use.

Annotations appear on:

* The [Charts](/docs/dashboard/charts) page, where you can also add, edit, and delete them.
* Charts pinned to the app's **Overview**.
* The **Results** chart of a campaign.

Overview pins and campaign results only display annotations. To change one, use the Charts page.

Charts without a date axis, such as a breakdown by country, a retention-period chart, or a **Totals** view, don't draw annotations.

### Where an annotation lands

Charts group data into UTC buckets, such as hours or days. An annotation is drawn in the bucket that contains its moment. For example, a release at 2:37 PM UTC is drawn on the 2 PM bucket of an hourly chart and on that day's bucket of a daily chart. A range covers every bucket it touches.

Hover a bucket to see its annotations. Their labels are listed at the top of the chart's tooltip, each marked in its color: a short dashed line for a point, a small square for a range.

![A Proceeds chart with two point annotations and a Spring sale range, with the tooltip listing Spring sale above the day](https://superwall.com/docs/images/charts/annotations-chart.jpg)

Annotations never get in the way of the chart. You can still drag across the chart to zoom into a date range.

### The Annotations control

The chart toolbar has an **Annotations** control, just before the chart type. It shows how many annotations the current chart draws, or **Hidden** if you've turned them off.

Click it to open the list of annotations. **In view** lists the ones in the chart's current date range, and **All** lists every annotation for the app. Both are newest first, and each row shows the annotation's label and when it happened, in your time zone.

![The Annotations control open, listing three annotations with edit and delete actions, the Show on charts switch, and Add annotation](https://superwall.com/docs/images/charts/annotations-control.jpg)

### Show or hide annotations

Use the **Show on charts** switch in the **Annotations** control to hide or show annotations. This setting only affects you, is saved in your current browser, and applies to every chart that draws annotations, including Overview pins and campaign results. It's on by default.

## Add, edit, and delete annotations

You need permission to write annotations. **Owners**, **Admins**, and **Editors** can add, edit, and delete them. **Readers** and **Analysts**, and members with the project **Viewer** role, can see annotations but don't get add, edit, or delete controls. See [Access Controls](/docs/dashboard/dashboard-settings/overview-settings-access-controls).

### Add an annotation

There are two ways to add an annotation on the Charts page:

* **From the chart:** Click a point on the chart, without dragging. A small **Add annotation** popover opens for that bucket. Type a label and click **Add annotation** to add a point there, or click **More options** to open the full form. Clicking next to an existing annotation opens that annotation instead; see [Edit or delete an annotation](#edit-or-delete-an-annotation).
* **From the Annotations control:** Open **Annotations** and click **Add annotation**.

![The Add annotation popover open on Oct 12 with a label typed in, and More options and Add annotation buttons](https://superwall.com/docs/images/charts/annotations-quick-add.jpg)

Clicking the chart only opens the popover when annotations are shown and the chart has a date axis.

The full form has these fields:

| Field                | What to enter                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------- |
| Label                | What happened, shown on the chart. Required, up to 191 characters.                          |
| Description          | Optional detail, up to 2,000 characters.                                                    |
| Type                 | **Point** for one moment, or **Range** for a span of time.                                  |
| When / Start and End | The moment for a point, or the start and end for a range. A range must end after it starts. |

Enter times in your own time zone. The form shows which time zone it's using, and Superwall saves each time as an exact moment, so everyone sees the annotation in the same place on the chart, whatever their time zone.

### Edit or delete an annotation

There are two ways to edit an annotation:

* **From the chart:** Click next to it. For a point, click anywhere in its bucket or right on its line. For a range, click in the bucket where it starts or where it ends. A small **Edit annotation** popover opens with its label. Change the label and click **Save changes**, or click **More options** to edit everything else. To add a new annotation there instead, click **Add new**. If several annotations are that close, the popover lists them so you can pick one.
* **From the Annotations control:** Open **Annotations** and use the edit action on an annotation's row.

![The Edit annotation popover opened by clicking the v4.2.0 released line, with its label, a delete icon, More options, Save changes, and Add new](https://superwall.com/docs/images/charts/annotations-quick-edit.jpg)

Clicking inside a range, away from its start and end, still adds a new annotation, so you can mark something that happened during it.

To delete an annotation, use **Delete annotation** in the edit form, the delete icon next to **More options** in the chart's **Edit annotation** popover, or the delete action on its row in **Annotations**.

![The Edit annotation form for the Spring sale range, with its label, description, Range type, start and end times, and Delete annotation at the left of the footer](https://superwall.com/docs/images/charts/annotations-edit-dialog.jpg)

Deleting asks you to confirm, then removes the annotation from every chart for the app. This is permanent and can't be undone. Every create, update, and delete is recorded in the organization's [Audit Log](/docs/dashboard/dashboard-settings/overview-settings-audit-log) with before and after values, so a deleted annotation's contents remain in the log.

## Manage annotations with the API

Use the Superwall API to add annotations from scripts or CI, for example to mark every release automatically. The full reference, with request and response schemas, is at [api.superwall.com/docs](https://api.superwall.com/docs).

### Authentication

1. Open **Settings > API Keys** in the Superwall dashboard.
2. Create an organization API key, or edit an existing one.
3. Give the key the `charts:write` scope to create, update, or delete annotations. Add `charts:read` if it also needs to list or retrieve them.
4. Copy the token when Superwall shows it.

> **Warning:** API keys created before chart annotations were released don't have the `charts:write` scope.
> Requests to create, update, or delete annotations with those keys return `403`. Edit the key to
> add `charts:write`, or create a new key with it.

Pass the token as a bearer token:

```bash
Authorization: Bearer YOUR_SECRET_TOKEN
```

### Endpoints

The base URL is `https://api.superwall.com`.

| Operation | Method and path                                                                    | Scope          |
| --------- | ---------------------------------------------------------------------------------- | -------------- |
| List      | `GET /v2/applications/{application_id}/chart_annotations`                          | `charts:read`  |
| Create    | `POST /v2/applications/{application_id}/chart_annotations`                         | `charts:write` |
| Retrieve  | `GET /v2/applications/{application_id}/chart_annotations/{chart_annotation_id}`    | `charts:read`  |
| Update    | `PATCH /v2/applications/{application_id}/chart_annotations/{chart_annotation_id}`  | `charts:write` |
| Delete    | `DELETE /v2/applications/{application_id}/chart_annotations/{chart_annotation_id}` | `charts:write` |

An application ID outside your organization, or an annotation ID that belongs to a different application, returns `404`.

### Timestamps

`start_at` and `end_at` are exact moments. Send them as ISO 8601 timestamps with a timezone, either `Z` or an offset such as `+02:00`. Superwall rejects these with `400`:

* Date-only values, such as `2026-10-28`.
* Values without a timezone, such as `2026-10-28T14:00:00`.
* Dates that don't exist, such as `2026-02-30T00:00:00Z`.
* Years before 2000 or after 9999.

Responses always return timestamps in UTC with milliseconds. For example, if you send `2026-10-28T16:00:00+02:00`, the response returns `2026-10-28T14:00:00.000Z`.

### Create an annotation

Send `label` and `start_at` to create a point. Add `end_at` to create a range.

<TypeTable
  type="{
  label: {
    type: &#x22;string&#x22;,
    description: &#x22;What happened, shown on the chart. 1 to 191 characters. Leading and trailing whitespace is removed, and a label of only whitespace is rejected.&#x22;,
    required: true,
  },
  description: {
    type: &#x22;string&#x22;,
    description: &#x22;Optional detail, up to 2,000 characters. A blank description is stored as `null`.&#x22;,
  },
  start_at: {
    type: &#x22;string&#x22;,
    description: &#x22;The moment, or the start of the range. An ISO 8601 timestamp with a timezone.&#x22;,
    required: true,
  },
  end_at: {
    type: &#x22;string | null&#x22;,
    description: &#x22;The end of the range. Must be after `start_at`; an equal or earlier value returns `400`. Omit it or send `null` to create a point.&#x22;,
    default: &#x22;null&#x22;,
  },
}"
/>

```bash
curl https://api.superwall.com/v2/applications/$APP_ID/chart_annotations \
  --request POST \
  --header "Authorization: Bearer $SUPERWALL_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "label": "Summer sale",
    "description": "20% off annual plans",
    "start_at": "2026-07-01T00:00:00Z",
    "end_at": "2026-07-08T00:00:00Z"
  }'
```

The response is the annotation. `id` and `application_id` are returned as strings:

```json
{
  "object": "chart_annotation",
  "id": "42",
  "application_id": "123",
  "label": "Summer sale",
  "description": "20% off annual plans",
  "start_at": "2026-07-01T00:00:00.000Z",
  "end_at": "2026-07-08T00:00:00.000Z",
  "created_at": "2026-06-30T18:12:05.000Z",
  "updated_at": "2026-06-30T18:12:05.000Z"
}
```

An application can hold up to 1,000 annotations. Creating one beyond that returns `400`. Delete an annotation first to make room.

### List annotations

List returns the application's annotations ordered by `start_at`. Use `from` and `to` to limit it to a window:

<TypeTable
  type="{
  from: {
    type: &#x22;string&#x22;,
    description: &#x22;Only annotations that end at or after this moment. A point ends where it starts.&#x22;,
  },
  to: {
    type: &#x22;string&#x22;,
    description: &#x22;Only annotations that start at or before this moment. Must not be before `from`.&#x22;,
  },
}"
/>

The window is inclusive and returns every annotation that overlaps it: a point at or between `from` and `to`, or a range that starts at or before `to` and ends at or after `from`.

> **Warning:** In a query string, a literal `+` is decoded as a space, so an offset like `+02:00` is rejected.
> Encode it as `%2B`, for example `from=2026-10-01T00:00:00%2B02:00`, or use UTC timestamps
> ending in `Z`.

```bash
curl --get https://api.superwall.com/v2/applications/$APP_ID/chart_annotations \
  --header "Authorization: Bearer $SUPERWALL_API_KEY" \
  --data-urlencode "from=2026-10-01T00:00:00Z" \
  --data-urlencode "to=2026-10-31T23:59:59Z"
```

The response includes a top-level `can_write` field that says whether the caller can create, update, and delete annotations. Use it to hide write controls in your own tools:

```json
{
  "object": "list",
  "data": [
    {
      "object": "chart_annotation",
      "id": "43",
      "application_id": "123",
      "label": "v4.2.0 released",
      "description": null,
      "start_at": "2026-10-28T14:00:00.000Z",
      "end_at": null,
      "created_at": "2026-10-28T14:00:03.000Z",
      "updated_at": "2026-10-28T14:00:03.000Z"
    }
  ],
  "can_write": true
}
```

### Update an annotation

`PATCH` changes only the fields you send and keeps the rest:

* Omit a field to keep its current value.
* Send `description: null` to remove the description.
* Send `end_at: null` to turn a range into a point.

The result must still end after it starts. If you move only `start_at` or only `end_at`, Superwall checks the combined result and returns `400` if it would end at or before its start.

```bash
curl https://api.superwall.com/v2/applications/$APP_ID/chart_annotations/$ANNOTATION_ID \
  --request PATCH \
  --header "Authorization: Bearer $SUPERWALL_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"end_at": "2026-07-10T00:00:00Z"}'
```

### Delete an annotation

`DELETE` removes the annotation from every chart. This is permanent. The change is recorded in the [Audit Log](/docs/dashboard/dashboard-settings/overview-settings-audit-log).

```bash
curl https://api.superwall.com/v2/applications/$APP_ID/chart_annotations/$ANNOTATION_ID \
  --request DELETE \
  --header "Authorization: Bearer $SUPERWALL_API_KEY"
```

```json
{
  "object": "chart_annotation",
  "id": "42",
  "deleted": true
}
```

## Mark releases from CI

Add a step to your release pipeline that creates a point annotation when a build ships. Store the API key as a CI secret, and generate `start_at` in UTC with `date -u +%Y-%m-%dT%H:%M:%SZ`:

```bash
curl https://api.superwall.com/v2/applications/$APP_ID/chart_annotations \
  --request POST \
  --header "Authorization: Bearer $SUPERWALL_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"label\":\"v$VERSION released\",\"start_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
```

The key only needs the `charts:write` scope. Restrict it to the project the app belongs to.

## Troubleshooting

| Status | What to check                                                                                                                                                                                       |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | A timestamp is missing its timezone or isn't a real date, `end_at` isn't after `start_at`, `to` is before `from`, the label is empty or too long, or the application already has 1,000 annotations. |
| `401`  | The request is missing a bearer token, or the token is invalid or revoked.                                                                                                                          |
| `403`  | The API key doesn't have the scope the operation needs: `charts:read` to list or retrieve, `charts:write` to create, update, or delete.                                                             |
| `404`  | The application isn't in your organization, or the annotation belongs to a different application.                                                                                                   |

## Related

* [Charts](/docs/dashboard/charts)
* [Access Controls](/docs/dashboard/dashboard-settings/overview-settings-access-controls)
* [Audit Log](/docs/dashboard/dashboard-settings/overview-settings-audit-log)
* [Superwall API](/docs/agents/api)