← Files RevenueCatARCHIVED FILE
skills/revenuecat-audiences/SKILL.md
16.1 KB · Oct 4, 2026 · 12:24 UTC
---
name: revenuecat-audiences
description: >
Use before sharing a link to a filtered customer list or audience, and when identifying,
filtering, or ranking the user's customers (a segment, who they are, a product or duration,
spend, renewals, status, country, attribution).
---
# Audience filters and dashboard links
Use Audiences to filter the user's customers and to share a dashboard link to that set. The same
filters answer "who" questions — they are not a full leaderboard sort.
There is no first-class CLI command for audiences. Do not tell the user RevenueCat cannot filter
or segment customers.
## Identifying, filtering, or ranking customers
Audiences filter on the [fields](#fields) below, including:
- Product and duration — `latestProduct`, `allPurchasedProductIds`, `latestPurchasedOffering`,
entitlements, offers
- Spend and renewals — `totalSpent`, `totalRenewals` (thresholds, not a sort)
- Subscription state — `status`, trial, auto-renew intent, ownership
- Store, platform, and country — `platform`, `latestStore`, `country`, `storefront`
- Dates — first seen, purchases, renewals, expiration, trial, cancellation
- Attribution and experiments — media source, campaign, ad group, keywords, price experiment
- Identity and custom attributes — app user ID, email, locale, `customAttribute:{key}`
Map the question onto those fields, filter, then read a sample.
1. Fetch project-specific values with `get-audience-filter-options` for any field marked
project-specific that the question uses (`project_id`, `fields`).
2. Reuse an existing audience from `list-audiences` if one already matches. Otherwise
`create-audience` with the same `groups`/`conditions` rule shape as [the filter rule](#the-filter-rule).
Body is only `name` and `rules`. `create-audience` persists a saved audience, so get explicit
confirmation first.
3. `get-audience` with `expand: ["customer_sample"]`. Sample rows include `total_spent`, status,
and latest product — not every filter field. Rank or name customers only from fields the
sample returned.
4. Number fields like `totalRenewals` are filters, not sample columns. Filter on a high
threshold and report the sample; do not invent a value that was not returned.
5. Say it is a sample of matches, not an exhaustive ranking of every customer. Filtering and a
`customer_sample` cannot prove a global superlative (who renewed or spent the most). After you
try the steps above, say that: you can show high-threshold matches, not name a unique maximum.
6. Share the dashboard link ([constructing a link](#constructing-a-link) or
[linking to a saved audience](#linking-to-a-saved-audience)).
## Constructing a link
Two shapes: a filtered `all-customers` link for ad-hoc exploration, and a saved-audience link
after `create-audience`.
1. Get the project ID from `list-projects`. For dashboard URLs, **strip the `proj` prefix**.
2. Pick fields and operators from the [field tables](#fields). Do not invent field or operator
names.
3. For project-specific fields, fetch valid values with `get-audience-filter-options` first.
4. Assemble the [rule JSON](#the-filter-rule): one group per OR-branch, conditions inside a group
for AND, values encoded per [value formats](#value-formats).
5. Serialize and URL-encode the rule with a short script (see [encoding the rule](#encoding-the-rule))
— do not encode by hand.
6. Append it as the `filters` query param on
`https://app.revenuecat.com/projects/{project_id}/customer-lists/all-customers`.
## URL format
```
https://app.revenuecat.com/projects/{project_id}/customer-lists/all-customers?filters={encoded_rule}
```
- `{project_id}` — short hex ID from `list-projects` with `proj` stripped.
- Filters only work on the `all-customers` list. The unfiltered Audiences home is
`/projects/{project_id}/customer-lists`.
## Linking to a saved audience
Link with `customer_list_id` — the field `create-audience`, `get-audience`, and `list-audiences`
return alongside `id`:
```
https://app.revenuecat.com/projects/{project_id}/customer-lists/{customer_list_id}
```
**An audience's `id` (`aud…`) and its `customer_list_id` (`list…`) are different identifiers.**
The dashboard route only resolves `customer_list_id`; an `aud…` id in that slot renders
"Audience not found".
**Correct:**
```
https://app.revenuecat.com/projects/56965ae1/customer-lists/list7c1f0a2b93
```
**Wrong** (the audience `id` instead of the `customer_list_id`):
```
https://app.revenuecat.com/projects/56965ae1/customer-lists/audf0269cdf3df84dd2
```
Do not add a `filters` param to a saved-audience link — the audience carries its own rules. If
the response has no `customer_list_id`, link to `/customer-lists` and name the audience rather
than guessing an id.
## Offering to save the filtered view
A filtered `all-customers` link is ad-hoc — nothing about it is saved. Say so in one short
sentence when you share one, and offer to save it: "This view isn't saved — want me to save it
as an audience so you can find it later?"
Offer once per conversation. If they accept, call `create-audience` with the same rule you built
for the link, then link to it. Do not offer when the link is already to a saved audience.
## The filter rule
The `filters` value is URL-encoded JSON with this shape:
```json
{
"groups": [
{
"conditions": [{ "field": "platform", "operator": "is", "value": "android" }]
}
]
}
```
- Conditions within a group combine with AND.
- Groups combine with OR.
- `value` is always a JSON string — booleans as `"true"`/`"false"`, numbers as `"42"`, lists as a
comma-separated string, date ranges and relative dates as stringified JSON (see
[value formats](#value-formats)).
**Correct** (compact JSON, whole value URL-encoded):
```
?filters=%7B%22groups%22%3A%5B%7B%22conditions%22%3A%5B%7B%22field%22%3A%22platform%22%2C%22operator%22%3A%22is%22%2C%22value%22%3A%22android%22%7D%5D%7D%5D%7D
```
**Wrong** (raw JSON, spaces, unencoded quotes/braces):
```
?filters={"groups": [{"conditions": [...]}]}
```
## Encoding the rule
```python
import json
from urllib.parse import quote
rule = {
"groups": [
{"conditions": [{"field": "platform", "operator": "is", "value": "android"}]},
]
}
print(quote(json.dumps(rule, separators=(",", ":")), safe=""))
```
## Fields
The Audiences preview table shows only **Customer**, **Subscription Status**,
**Auto-Renewal Status**, **Spent**, and **Latest Purchase**. Other attributes appear in the CSV
from **Export all**, or on a customer's profile.
Use these exact `field` strings. See
[Audiences](https://www.revenuecat.com/docs/dashboard-and-metrics/audiences).
### Text fields
Operators: `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty`.
| Field | Meaning |
| ------------------------- | --------------------------------- |
| `customerId` | App user ID |
| `originalAppUserId` | Original app user ID |
| `email` | Email |
| `phoneNumber` | Phone number |
| `locale` | Locale |
| `appVersion` | App version |
| `sdkVersion` | SDK version |
| `platformVersion` | Platform (OS) version |
| `projectId` | Project ID (e.g. `proj1ab2c3d4`) |
| `projectName` | Project name |
| `appConfigId` | App ID (e.g. `app1ab2c3d4`) |
| `appConfigName` | App name |
| `idfa` | IDFA |
| `idfv` | IDFV |
| `gpsAdId` | GPS ad ID |
| `latestPurchasedOffering` | Latest purchased offering |
| `latestOffer` | Latest offer identifier |
| `latestEntitlements` | Latest entitlement identifiers |
| `allPurchasedProductIds` | All purchased product identifiers |
### Enum fields
Operators: `is`, `isNot`, `isAnyOf`, `isNotAnyOf`, `isEmpty`, `isNotEmpty`.
| Field | Values |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `platform` | `iOS`, `android`, `web`, `macOS`, `amazon`, `roku`, `tvOS`, `visionOS`, `watchOS` |
| `status` | `active`, `trialing`, `in_grace_period`, `in_billing_retry`, `paused`, `expired`, `incomplete`, `unknown` |
| `latestStore` | `app_store`, `play_store`, `promotional`, `mac_app_store`, `stripe`, `amazon`, `roku`, `rc_billing`, `paddle`, `external` |
| `anyActiveStore` | Any active store — same store identifiers as `latestStore` |
| `latestOwnershipType` | `PURCHASED`, `FAMILY_SHARED` |
| `latestOfferType` | `no_offer`, `free_trial`, `introductory_offer`, `offer_code`, `promotional_offer`, `win_back_offer`, `unspecified_offer` |
| `priceExperimentVariant` | `a`, `b`, `c`, `d` |
| `country` | Last seen country — ISO 3166-1 alpha-2 codes (e.g. `US`, `DE`) |
| `latestStoreCountry` | ISO 3166-1 alpha-2 country codes |
| `storefront` | Store country — ISO 3166-1 alpha-2 codes |
| `mediaSource` | project-specific — [fetch valid values](#fetching-project-specific-values) |
| `campaign` | project-specific |
| `adGroup` | project-specific |
| `ad` | project-specific |
| `keyword` | project-specific |
| `creative` | project-specific |
| `priceExperimentId` | project-specific |
| `latestProduct` | product IDs of the project — fetch valid values |
### Boolean fields
Operators: `is`, `isNot`. Value is exactly `"true"` or `"false"`.
| Field | Meaning |
| -------------------------------- | ---------------------------------------------------- |
| `hasMadeSandboxPurchase` | Has made a sandbox purchase |
| `hasMadeNonSubscriptionPurchase` | Has made a non-subscription purchase |
| `latestAutoRenewIntent` | Auto-renewal status (`true` = set to renew) |
| `isCurrentlyTrialing` | Currently trialing |
| `isRcPromo` | Has been granted an entitlement via RC (promotional) |
### Number fields
Operators: `equal`, `notEqual`, `greaterThan`, `greaterThanOrEqual`, `lessThan`,
`lessThanOrEqual`, `isEmpty`, `isNotEmpty`. Value is a numeric string, e.g. `"50"`.
| Field | Meaning |
| --------------- | ------------------------ |
| `totalSpent` | Total spent |
| `totalRenewals` | Total number of renewals |
### Date fields
Operators: `before`, `beforeOrOn`, `on`, `after`, `afterOrOn`, `within`, `between`, `notBetween`,
`isEmpty`, `isNotEmpty`.
| Field | Meaning |
| ---------------------- | ------------------------------ |
| `firstSeenAt` | First seen |
| `lastSeenAt` | Last seen |
| `firstPurchaseAt` | First purchase |
| `mostRecentPurchaseAt` | Most recent purchase |
| `mostRecentRenewalAt` | Most recent renewal |
| `latestExpirationAt` | Latest expiration |
| `trialStartAt` | Trial start |
| `trialEndAt` | Trial end |
| `subscriptionOptOutAt` | Most recent cancellation |
| `trialOptOutAt` | Most recent trial cancellation |
### Custom attribute fields
Filter with `customAttribute:{key}` (e.g. `customAttribute:favorite_team`). They use the enum
operators. Fetch known keys and values with `get-audience-filter-options` — never invent a key.
## Value formats
- `isEmpty` / `isNotEmpty` — set `"value": ""` (the value is ignored).
- `isAnyOf` / `isNotAnyOf` — comma-separated string: `"value": "US,CA,MX"`.
- `before`, `beforeOrOn`, `on`, `after`, `afterOrOn` — calendar date `"value": "2026-01-31"`
(`YYYY-MM-DD`).
- `between` / `notBetween` — stringified JSON with exactly `from` and `to`:
`"value": "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}"` (`from` ≤ `to`).
- `within` — stringified JSON with exactly `direction`, `value`, `unit`:
`"value": "{\"direction\":\"last\",\"value\":30,\"unit\":\"days\"}"`. `direction` is `last` or
`next`; `unit` is `minutes`, `hours`, or `days`; `value` is a non-negative integer.
`before`/`beforeOrOn`/`after`/`afterOrOn` also accept this relative format (not `on`).
## Fetching project-specific values
Fields marked project-specific (`mediaSource`, `campaign`, `adGroup`, `ad`, `keyword`, `creative`,
`priceExperimentId`, `latestProduct`) and custom attributes only match values that exist in the
project's data. Fetch with `get-audience-filter-options`:
- `project_id` (required)
- `fields` (required, at least one) — any of the eight fields above, `customAttribute:{key}` for
one custom attribute, or `customAttribute` to list every custom attribute key with its values.
A custom-attribute entry may come back with `cardinality_exceeded: true` — a value the user
stated verbatim can still be valid even if it is not in the list.
Fixed-value fields (`country`, `platform`, `status`, …) are not served by this tool — use the
tables above. Never guess project-specific values — a filter on a non-existent value silently
matches zero customers.
## Example: building a link
User wants: "Android customers acquired through Instagram" in project `proj56965ae1`.
Rule (both conditions in one group — AND):
```json
{
"groups": [
{
"conditions": [
{ "field": "platform", "operator": "is", "value": "android" },
{ "field": "mediaSource", "operator": "is", "value": "Instagram" }
]
}
]
}
```
Link:
```
https://app.revenuecat.com/projects/56965ae1/customer-lists/all-customers?filters=%7B%22groups%22%3A%5B%7B%22conditions%22%3A%5B%7B%22field%22%3A%22platform%22%2C%22operator%22%3A%22is%22%2C%22value%22%3A%22android%22%7D%2C%7B%22field%22%3A%22mediaSource%22%2C%22operator%22%3A%22is%22%2C%22value%22%3A%22Instagram%22%7D%5D%7D%5D%7D
```
User wants: "customers on iOS or Android who are currently trialing and were first seen in the
last 30 days".
```json
{
"groups": [
{
"conditions": [
{ "field": "platform", "operator": "isAnyOf", "value": "iOS,android" },
{ "field": "isCurrentlyTrialing", "operator": "is", "value": "true" },
{
"field": "firstSeenAt",
"operator": "within",
"value": "{\"direction\":\"last\",\"value\":30,\"unit\":\"days\"}"
}
]
}
]
}
```
SHA-256: 5624356a2e922c153600f27317d7dbdd9328e91346a8eb415319e7fb9ea07d14