Adapty Developer CLI. Manage apps, products, paywalls, placements, flows, and access levels from your terminal.
npm install -g adaptyRequires Node.js >= 18.
adapty auth loginOpens browser for OAuth device flow. Token is stored in ~/.config/adapty/config.json.
Override with ADAPTY_TOKEN environment variable:
ADAPTY_TOKEN=your-token adapty apps listOther auth commands:
adapty auth whoami # verify token, show user info
adapty auth status # show local auth state
adapty auth logout # clear stored token (local only)
adapty auth revoke # revoke token server-side and clear localAll resource commands require --app APP_ID (UUID). Use adapty apps list to find your app ID.
adapty apps list [--page N] [--page-size N]
adapty apps get APP_ID
adapty apps create --title "My App" --platform ios --apple-bundle-id com.example.app
adapty apps update APP_ID [flags]adapty products list --app UUID [--page N] [--page-size N]
adapty products get --app UUID PRODUCT_ID
adapty products create --app UUID [flags]
adapty products update --app UUID PRODUCT_ID [flags]adapty paywalls list --app UUID [--page N] [--page-size N]
adapty paywalls get --app UUID PAYWALL_ID
adapty paywalls create --app UUID --title "Name" --product-id UUID1 [--product-id UUID2]
adapty paywalls update --app UUID PAYWALL_ID [flags]adapty placements list --app UUID [--page N] [--page-size N]
adapty placements get --app UUID PLACEMENT_ID
adapty placements create --app UUID [flags]
adapty placements update --app UUID PLACEMENT_ID [flags]adapty flows list --app UUID [--page N] [--page-size N]
adapty flows get --app UUID FLOW_ID
adapty flows create --app UUID --name "Name"
adapty flows config get --app UUID FLOW_ID
adapty flows config update --app UUID FLOW_ID (--config JSON | --config-file PATH|-) [--remote-configs JSON] [--expected-updated-at MS]A freshly created flow has no config until the first flows config update; flows config get returns 404
until then. Pass --expected-updated-at (the updated_at from a prior config get) to fail instead of
overwriting a concurrent dashboard edit.
adapty access-levels list --app UUID [--page N] [--page-size N]
adapty access-levels get --app UUID ACCESS_LEVEL_ID
adapty access-levels create --app UUID [flags]
adapty access-levels update --app UUID ACCESS_LEVEL_ID [flags]Apple Search Ads commands live under adapty asa and talk to the ASA service rather than the Developer
API. They take no --app: the scope is the company behind your token. A connected Apple Ads account and
an active Ads Manager subscription are required — adapty asa whoami tells you where you stand.
adapty asa whoami # company, how access was granted, Apple connection state
adapty asa connect [--no-wait] # link an Apple Search Ads account
adapty asa apps list # apps promoted in Apple Search Ads
adapty asa orgs list # Apple Search Ads organizationsEvery list takes scope filters, and they narrow the query rather than the printed page — asa keywords list
unfiltered pages through the whole account, while one ad group is a handful of rows, so scope the read:
adapty asa campaigns list --app APP_UUID --status PAUSED
adapty asa ad-groups list --campaign CAMPAIGN_UUID
adapty asa keywords list --ad-group AD_GROUP_UUID --status ACTIVE
adapty asa keywords list --ad-group AD_GROUP_UUID --ad-group OTHER_UUID # repeatable
adapty asa creatives list --app APP_UUID--campaign-group, --app, --campaign, --ad-group are repeatable and take the UUIDs printed by the
matching list command; --search matches names case-insensitively. Each list accepts only the filters that
make sense for it: --ad-group starts at keywords, negative keywords, search terms and ads, --status is
ENABLED/PAUSED everywhere except keywords, which are ACTIVE/PAUSED, and asa ads list has no --app
because ads hang off ad groups. An id belonging to another company simply matches nothing.
Campaign structure. These lists return metadata only — numbers come from asa metrics, and only
asa search-terms list takes --date-from / --date-to (default: today):
adapty asa campaigns list
adapty asa campaigns get CAMPAIGN_ID
adapty asa campaigns create --org UUID --name "Winter push" --adam-id 123456 --country US --daily-budget 50
adapty asa campaigns create --org UUID --name "LOC push" --adam-id 123456 --country US --daily-budget 50 \
--invoice-advertiser "Acme Inc" --invoice-order-number PO-42 --invoice-contact-name "Jane Doe" \
--invoice-contact-email jane@acme.com --invoice-billing-email billing@acme.com # payment_model LOC
adapty asa campaigns update CAMPAIGN_ID [--status PAUSED] [--daily-budget 80] [--country US]
adapty asa ad-groups list
adapty asa ad-groups get AD_GROUP_ID
adapty asa ad-groups create --campaign UUID --name "Brand terms" --default-bid 1.20
adapty asa ad-groups create --campaign UUID --name "Automated Max Conv" --automated # Max Conversions campaigns
adapty asa ad-groups update AD_GROUP_ID [--default-bid 1.50] [--status PAUSED]
adapty asa ads list
adapty asa ads get AD_ID
adapty asa ads create --ad-group UUID --creative-id 4321 --name "Summer ad"
adapty asa ads update AD_ID [--name "..."] [--status PAUSED]Bulk structure creation submits a whole tree — campaigns with nested ad groups, keywords, negative keywords
and ads — in one call. The input is a JSON structure (the natural path for an AI agent: generate it, pipe it
in), or a native Apple Ads template converted server-side. The whole payload is validated before anything is
created — a rejection lists every bad node; on acceptance the command polls progress and prints the final
report (success / partial / failed, with per-object failures):
adapty asa campaigns bulk-create --file structure.json # JSON structure, waits and reports
cat structure.json | adapty asa campaigns bulk-create --file - # same, from stdin
adapty asa campaigns bulk-create --file structure.json --no-wait
adapty asa campaigns bulk-create --from-file Campaign_And_Adgroup_Template.xlsx --org-id 1234567
adapty asa campaigns bulk-create --from-file keywords_template.csv --org-id 1234567 --preview
adapty asa campaigns bulk-status OPERATION_IDThe structure is {campaign_group_id | campaign_group_internal_id, campaigns: [...]} where each campaign
node either creates (payload) or addresses an existing campaign by id (an anchor, optionally carrying
update_payload), and nests ad_groups with keywords, negative_keywords and ads the same way.
--from-file takes the native Apple Ads templates and needs --org-id (the Apple org id from
asa orgs list); --preview prints the converted request without creating anything. Conversion issues are
reported with their sheet, row and column.
Keywords are always applied as a batch, at most 100 per call, and a partial rejection is reported per item:
adapty asa keywords list
adapty asa keywords add --ad-group UUID --text "running shoes" --text "trail shoes" [--bid 1.20] [--match-type EXACT]
adapty asa keywords add --ad-group UUID --from-file keywords.txt
adapty asa keywords update KEYWORD_ID [KEYWORD_ID...] [--bid 2.00] [--status PAUSED]
adapty asa negative-keywords list
adapty asa negative-keywords add --ad-group UUID --text free
adapty asa negative-keywords add --campaign UUID [--all-ad-groups] --text free
adapty asa search-terms list [--date-from ... --date-to ...]Product pages and rule-based automations:
adapty asa product-pages list
adapty asa product-pages sync [--adam-id 123456]
adapty asa automations list
adapty asa automations get AUTOMATION_ID
adapty asa automations create --file rule.json [--run-now]
adapty asa automations update AUTOMATION_ID [--stop] [--start] [--name "..."] [--file rule.json]
adapty asa automations run AUTOMATION_ID [--dry-run]
adapty asa automations runs AUTOMATION_IDMetrics take an entity level, a period and an optional metric selection. Rows come back one per entity,
aggregated and sorted server-side, so a top-N or a breakdown is a single call — use --order-by with a small
--page-size for rankings, metrics overview for account totals and time series, and one big page (up to
1000 rows) when you genuinely need every row; never sum pages client-side:
adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31
adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --order-by spend --page-size 5
adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --group-by country --page-size 1000
adapty asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric roas
adapty asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90
adapty asa metrics overview --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 [--period-unit week]There is no ltv metric: lifetime value is a cohort metric read at a renewal window, so --by-days is how you
ask for day-7 or day-90 values — on either route, up to 16 windows per call. --order-by-day ranks the rows by
one of those windows, which is how you get the top campaigns by day-90 ROAS in a single call.
Competitor summary takes 1–5 Apple App Store IDs and covers the last full month across every country — there are no period or country flags on purpose. The first call on a cold cache can take tens of seconds:
adapty asa competitors summary --app-ids 111111111,2222222Writes go straight to Apple and take seconds, so every writing command first prints the exact request body and
asks for a yes. --yes skips the question for scripts; in a pipe or under --json the command refuses instead
of waiting for input that will never come. There is no undo — the CLI has no delete.
Every write also carries an idempotency key. The CLI generates one per invocation and retries once on a
network error, so a request that died on the wire is never applied twice. Pass --idempotency-key to pin the
key yourself: re-running a script with the same key within 24 hours replays the stored result — the CLI prints
"Already applied earlier — showing the stored result." — instead of creating a second entity. The same key
with a different body is rejected (422 cli_idempotency_key_reuse), and a concurrent duplicate answers
409 cli_idempotency_in_progress.
Analytics is rate limited per company: the metrics routes get 5 calls a minute (at most 2 in any 10 seconds)
and share a pool of two concurrent queries with the search-terms list — a busy pool answers
429 cli_analytics_busy, an exhausted window 429 cli_rate_limit_exceeded, both with the exact wait in
Retry-After. The CLI absorbs a single 429 on its own — it waits the announced Retry-After (up to 60
seconds; cool-downs are never waited out) and retries once — so a 429 that reaches you means the retry failed
too. A burst of 429s puts the token into an escalating cool-down (cli_cooldown_active, 5 minutes →
30 minutes → 3 hours); retries during the pause don't extend it, but the cure is fixing the failing request,
not waiting out the pause in a loop. An automation run is queued rather than awaited: run prints a run ID
and the outcome shows up in adapty asa automations runs.
| Flag | Description |
|---|---|
--json |
Output as JSON |
--help |
Show help |
--page |
Page number (default: 1) |
--page-size |
Items per page (default: 20, max: 100; asa commands: default 100, max 1000) |
adapty flows config preview <config_file> turns a local flow config into a render URL. On a TTY it opens the
browser; piped or with --json it prints the URL alone. Screenshotting is the caller's job — the CLI only
builds the URL.
Small configs only. This is a quick-look escape hatch. The config travels in the URL fragment, and past roughly 32KB of pretty-printed JSON the render page becomes slow and unreliable. Trim to the screen you care about, or preview the saved flow in the dashboard builder instead.
That URL is huge. Even a config the page renders happily produces thousands of characters, and a 668KB flow yields ~113,000. Nobody should read it — agents especially should never let it into their context. Pipe it into whatever captures the screenshot:
adapty flows config preview flow.json --screen scr_abc | node capture.mjs --out shot.png # or, for a tool that wants a flag instead of stdin: node capture.mjs --url "$(adapty flows config preview flow.json --screen scr_abc)" --out shot.pngPrefer the pipe: it has no size limit, while an argument is capped by the shell (~1MB, so a config around 6MB).
See skills/adapty-cli/references/cli-commands.md for the flags and the size ceiling.
| Variable | Description |
|---|---|
ADAPTY_TOKEN |
Override stored auth token |
ADAPTY_API_URL |
Override Developer API base URL (default: https://api-admin.adapty.io/api/v1/developer) |
ADAPTY_ASA_API_URL |
Override Apple Search Ads base URL (default: https://api-asa-admin.adapty.io/api/v1/cli) |
ADAPTY_APP_URL |
Override dashboard base URL (default: https://app.adapty.io). Used by flows config preview for the fixed /flow-preview route, and by auth login to keep the verification link on that host |
The two API URLs are independent: pointing ADAPTY_API_URL at a staging host leaves adapty asa on the ASA
default, and the other way round.
Install the Adapty CLI skill for Claude Code:
npx skills add adaptyteam/adapty-cli --skill adapty-clipnpm install
pnpm build
./bin/run.js apps listMIT