Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,15 @@ Campaign structure. These lists return metadata only — numbers come from `asa
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
Expand Down
42 changes: 38 additions & 4 deletions docs/agent/asa-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,61 @@ Every `list` and `get` command in this file returns metadata only, no metrics. E
| `asa whoami` | none | Company, how access was granted, Apple connection state. Run this first. No connected Apple Ads account or no active Ads Manager subscription answers `402 ads_manager_subscription_required` on every other `asa` command. |
| `asa connect` | `--no-wait` optional | Prints the Apple authorization link and waits for the link to be completed; `--no-wait` returns immediately instead of waiting. |
| `asa apps list` | pagination only | Apps promoted in Apple Search Ads. Its rows supply `--adam-id` for `campaigns create`. |
| `asa orgs list` | pagination only | Apple Search Ads organizations. Each row carries two identifiers, not interchangeable: `internal_id` (a UUID) and `org_id` (Apple's numeric id). `--org` on `campaigns create` takes `internal_id` — passing the numeric `org_id` fails with "Invalid org ID format." |
| `asa orgs list` | pagination only | Apple Search Ads organizations. Each row carries two identifiers, not interchangeable: `internal_id` (a UUID) and `org_id` (Apple's numeric id). `--org` on `campaigns create` takes `internal_id` — passing the numeric `org_id` fails with "Invalid org ID format." `payment_model` (`LOC`/`PAYG`) tells you whether campaigns in that organization need Invoicing Options — see [Line of credit](#line-of-credit-loc-organizations). |

## Campaigns

| Command | Flags | Notes |
|---|---|---|
| `asa campaigns list` | scope filters only | Metadata only. |
| `asa campaigns get <id>` | positional UUID | Metadata only. |
| `asa campaigns create` | `--org`, `--name`, `--adam-id`, `--country` (repeatable), `--daily-budget`; optional `--status` (`ENABLED`/`PAUSED`, no default), `--budget` (lifetime), `--target-cpa`, `--bidding-strategy`, `--supply-source` (repeatable, default `APPSTORE_SEARCH_RESULTS`), `--billing-event` (`IMPRESSIONS`/`TAPS`, default `TAPS`), `--ad-channel-type` (`DISPLAY`/`SEARCH`, default `SEARCH`) | `--org` takes the UUID (`internal_id`) from `asa orgs list`, not that row's numeric `org_id`; `--adam-id` comes from `asa apps list`. `--status` has no default — pass `--status PAUSED` to launch without spending until you enable it. |
| `asa campaigns update <id>` | at least one of `--name`, `--status`, `--country`, `--daily-budget`, `--budget`, `--target-cpa`, `--bidding-strategy` | |
| `asa campaigns create` | `--org`, `--name`, `--adam-id`, `--country` (repeatable), `--daily-budget`; optional `--status` (`ENABLED`/`PAUSED`, no default), `--budget` (lifetime), `--target-cpa`, `--bidding-strategy`, `--supply-source` (repeatable, default `APPSTORE_SEARCH_RESULTS`), `--billing-event` (`IMPRESSIONS`/`TAPS`, default `TAPS`), `--ad-channel-type` (`DISPLAY`/`SEARCH`, default `SEARCH`), `--invoice-advertiser`, `--invoice-order-number`, `--invoice-contact-name`, `--invoice-contact-email`, `--invoice-billing-email` (all five together, LOC organizations only) | `--org` takes the UUID (`internal_id`) from `asa orgs list`, not that row's numeric `org_id`; `--adam-id` comes from `asa apps list`. `--status` has no default — pass `--status PAUSED` to launch without spending until you enable it. The response carries `serving_status` and `serving_state_reasons`; when the campaign is `NOT_RUNNING` the command prints the reason and the fixing command — see [Max Conversions](#max-conversions-campaigns) and [Line of credit](#line-of-credit-loc-organizations). |
| `asa campaigns update <id>` | at least one of `--name`, `--status`, `--country`, `--daily-budget`, `--budget`, `--target-cpa`, `--bidding-strategy`, or `--invoice-advertiser`, `--invoice-order-number`, `--invoice-contact-name`, `--invoice-contact-email`, `--invoice-billing-email` together | The `--invoice-*` flags replace the stored Invoicing Options as a whole — pass all five, a partial set exits 2 before the network. |
| `asa campaigns bulk-create` | exactly one of `--file` (JSON structure, `-` for stdin) / `--from-file` (Apple Ads template, `.xlsx` or keywords `.csv`); `--org-id` required with `--from-file`; optional `--preview`, `--no-wait`, `--poll-interval` (default `5`), `--timeout` (default `900`) | Creates a whole structure — campaigns → ad groups → keywords/negative keywords/ads — as one queued operation. `--org-id` is the exception to this file's UUID rule: it takes the **numeric** `org_id` from `asa orgs list` (Apple's `campaign_group_id`), not the `internal_id` UUID that `campaigns create --org` takes. `--from-file` converts the template server-side first (its own budget — see [Request budgets](#request-budgets)); with `--preview` the command prints the converted request and creates nothing. By default it polls until the operation finishes (`success`/`partial`/`failed`, per-object failures listed); `--no-wait` prints the `operation_id` and returns — follow up with `bulk-status`. |
| `asa campaigns bulk-status <operation-id>` | positional operation id, printed by `bulk-create` | Progress of one bulk operation: status, applied/failed counts, and the per-object log with each failure's reason. |
| `asa campaigns bulk-list` | optional `--status` (`pending`/`running`/`success`/`partial`/`failed`, repeatable), `--app` (UUID), `--created-from`/`--created-to` (YYYY-MM-DD) | This company's bulk operations, newest first — one row per operation with its verdict and timestamps, no per-object detail. Use it to find an `operation_id` you lost or to check what ran recently, then drill in with `bulk-status`. Cheap catalog read. |

### Max Conversions campaigns

A campaign with `--bidding-strategy MAX_CONVERSIONS` is created fine but stays `NOT_RUNNING` with
`serving_state_reasons: ["AUTOMATED_KEYWORDS_REQUIRED_AD_GROUP_MISSING"]` until it has an **automated ad group**.
Always create the pair:

```sh
adapty asa campaigns create --org ORG_UUID --name "Max Conv" --adam-id 123456 --country US --daily-budget 50 --bidding-strategy MAX_CONVERSIONS
adapty asa ad-groups create --campaign CAMPAIGN_UUID --name "Automated Max Conv" --automated
```

`--automated` sends `automated_keywords_required: true` and `automated_keywords_opt_in: true`, omits `start_time`
(Apple schedules the automated group itself — combining it with `--start-time` exits 2), must stay `ENABLED`
(`--status PAUSED` exits 2; pause the campaign instead) and makes `--default-bid` optional (Apple reports the bid as
`0` when omitted). A plain ad group, even with `--automated-keywords`, does not satisfy the requirement. On the
campaign, `--target-cpa` must be lower than `--daily-budget`.

### Line of credit (LOC) organizations

`asa orgs list` shows each organization's `payment_model`. When it is `LOC`, Apple requires Invoicing Options on
every campaign — without them the campaign is created but sits `NOT_RUNNING` with
`serving_state_reasons: ["MISSING_BO_OR_INVOICING_FIELDS"]`. Pass all five flags together (a partial set exits 2
before the network):

```sh
adapty asa campaigns create --org 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
```

For a campaign that already exists, the same five flags on `asa campaigns update <id>` set (or replace) its
Invoicing Options. They map to `loc_invoice_details` in the request: advertiser → `client_name`, order number →
`order_number`, contact name → `buyer_name`, contact email → `buyer_email`, billing email → `billing_contact_email`.

## Ad groups

| Command | Flags | Notes |
|---|---|---|
| `asa ad-groups list` | scope filters only | Metadata only. |
| `asa ad-groups get <id>` | positional UUID | Metadata only. |
| `asa ad-groups create` | `--campaign`, `--name`, `--default-bid` | Apple also requires a pricing model and a start time; the CLI defaults `--pricing-model` to `CPC` (the only other option is `CPM`) and `--start-time` to today if you don't pass them. |
| `asa ad-groups create` | `--campaign`, `--name`, `--default-bid` (optional with `--automated`); optional `--automated` | Apple also requires a pricing model and a start time; the CLI defaults `--pricing-model` to `CPC` (the only other option is `CPM`) and `--start-time` to today if you don't pass them. `--automated` creates the automated ad group a Max Conversions campaign needs: it implies automated keywords, sends no `start_time` (exits 2 if `--start-time` is also given), must be `ENABLED` (exits 2 on `--status PAUSED`) and does not require `--default-bid` — see [Max Conversions](#max-conversions-campaigns). |
| `asa ad-groups update <id>` | at least one field | The campaign is resolved server-side and is never passed on update. |

## Ads
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "adapty",
"description": "Adapty command line interface",
"version": "0.8.1",
"version": "0.8.2",
"author": "Adapty team <support@adapty.io>",
"bin": {
"adapty": "./bin/run.js"
Expand Down
12 changes: 12 additions & 0 deletions skills/adapty-cli/references/asa-agent-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ adapty asa keywords update KW_UUID [KW_UUID...] --bid 2.00 --yes
adapty asa competitors summary --app-ids 1668337467,6503873027
```

**"Create a Max Conversions campaign"** — two writes, always both: Apple keeps an MC campaign
`NOT_RUNNING` (`AUTOMATED_KEYWORDS_REQUIRED_AD_GROUP_MISSING`) until it owns an automated ad group.
Check `payment_model` in `asa orgs list` first — a `LOC` organization also needs the five `--invoice-*`
flags on the campaign, or it stays `NOT_RUNNING` with `MISSING_BO_OR_INVOICING_FIELDS`:
```sh
adapty asa campaigns create --org ORG_UUID --name "Max Conv" --adam-id 123456 --country US --daily-budget 50 --bidding-strategy MAX_CONVERSIONS
adapty asa ad-groups create --campaign CAMPAIGN_UUID --name "Automated Max Conv" --automated
```
Read `serving_status` / `serving_state_reasons` in the response: the command prints the fixing command
for the reasons above. For an existing LOC campaign, `asa campaigns update <id> --invoice-*...` sets the
Invoicing Options.

## What the failed sessions did wrong (do not repeat)

- Looped `--page 1..4` to build an account total → burned the 5/min budget, hit 429s, gave up.
Expand Down
8 changes: 4 additions & 4 deletions skills/adapty-cli/references/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ every `asa` command answers `402 ads_manager_subscription_required`. Start with
| `asa whoami` | company, how access was granted, Apple connection state |
| `asa connect` | prints the Apple authorization link and waits; `--no-wait` returns at once |
| `asa apps list` | (pagination only) |
| `asa orgs list` | ASA organizations; their ID is the `--org` of `campaigns create` |
| `asa orgs list` | ASA organizations; their ID is the `--org` of `campaigns create`; `payment_model: LOC` means campaigns need the five `--invoice-*` flags |
| `asa campaigns list` | metadata only, no metrics; filters below |
| `asa campaigns get <campaign_id>` | positional UUID |
| `asa campaigns create` | `--org`, `--name`, `--adam-id`, `--country` (repeatable), `--daily-budget`; optional `--target-cpa`, `--bidding-strategy` |
| `asa campaigns update <campaign_id>` | at least one of `--name`, `--status`, `--country`, `--daily-budget`, `--budget`, `--target-cpa`, `--bidding-strategy` |
| `asa campaigns create` | `--org`, `--name`, `--adam-id`, `--country` (repeatable), `--daily-budget`; optional `--target-cpa`, `--bidding-strategy`; LOC orgs: all five `--invoice-*` flags. A `MAX_CONVERSIONS` campaign also needs `ad-groups create --automated` or it stays `NOT_RUNNING` (`AUTOMATED_KEYWORDS_REQUIRED_AD_GROUP_MISSING`) |
| `asa campaigns update <campaign_id>` | at least one of `--name`, `--status`, `--country`, `--daily-budget`, `--budget`, `--target-cpa`, `--bidding-strategy`, or the five `--invoice-*` flags together (fixes `MISSING_BO_OR_INVOICING_FIELDS`) |
| `asa ad-groups list` / `get <id>` | metadata only, like campaigns; numbers come from `asa metrics` |
| `asa ad-groups create` | `--campaign`, `--name`, `--default-bid`; Apple also needs `--pricing-model` (default CPC) and `--start-time` (default today) |
| `asa ad-groups create` | `--campaign`, `--name`, `--default-bid`; Apple also needs `--pricing-model` (default CPC) and `--start-time` (default today). `--automated` = the automated group for Max Conversions: no `--start-time`, no `--status PAUSED`, `--default-bid` optional |
| `asa ad-groups update <id>` | at least one field; the campaign is resolved server-side, never passed |
| `asa keywords list` | metadata only; **filter by `--ad-group`** — the heaviest read, own budget (30/min, 2 concurrent, 60s cap) |
| `asa keywords add` | `--ad-group` plus `--text` (repeatable) and/or `--from-file`; max 100 per call |
Expand Down
26 changes: 22 additions & 4 deletions src/commands/asa/ad-groups/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,57 @@ import {isValidUuid} from '../../../lib/flags.js'
import {printResponse} from '../../../lib/output.js'

export default class AsaAdGroupsCreate extends Command {
static description = 'Create an ad group inside a campaign'
static description =
'Create an ad group inside a campaign; --automated creates the automated ad group a Max Conversions campaign needs to run'
static enableJsonFlag = true
static examples = [
'<%= config.bin %> asa ad-groups create --campaign UUID --name "Brand terms" --default-bid 1.20',
'<%= config.bin %> asa ad-groups create --campaign UUID --name "Brand terms" --default-bid 1.20 --start-time 2026-09-01 --pricing-model CPM',
'<%= config.bin %> asa ad-groups create --campaign UUID --name "Automated Max Conv" --automated',
]
static flags = {
...currencyFlag,
...scheduleFlags,
...pricingModelFlag,
...confirmFlags,
...idempotencyFlags,
automated: Flags.boolean({
description: 'Create the automated ad group a Max Conversions campaign needs (no bid, no schedule, always ENABLED)',
exclusive: ['automated-keywords'],
}),
'automated-keywords': Flags.boolean({allowNo: true, description: 'Let Apple add keywords automatically'}),
campaign: Flags.string({description: 'Campaign ID (UUID)', required: true}),
'cpa-goal': moneyFlag('CPA goal'),
'default-bid': moneyFlag('Default bid', {required: true}),
'default-bid': moneyFlag('Default bid; required unless --automated is set'),
name: Flags.string({description: 'Ad group name', required: true}),
status: Flags.string({description: 'Initial status', options: ['ENABLED', 'PAUSED']}),
}

async run(): Promise<AsaAdGroupMutationDTO> {
const {flags} = await this.parse(AsaAdGroupsCreate)
if (!isValidUuid(flags.campaign)) this.error('Invalid campaign ID format.', {exit: 2})
if (!flags.automated && flags['default-bid'] === undefined) {
this.error('--default-bid is required unless --automated is set.', {exit: 2})
}

if (flags.automated && flags['start-time'] !== undefined) {
this.error('--start-time is not allowed with --automated: Apple schedules the automated ad group itself.', {exit: 2})
}

if (flags.automated && flags.status === 'PAUSED') {
this.error('An automated ad group must be ENABLED; pause the campaign instead.', {exit: 2})
}

const body = {
automated_keywords_opt_in: flags['automated-keywords'],
automated_keywords_opt_in: flags.automated ? true : flags['automated-keywords'],
automated_keywords_required: flags.automated ? true : undefined,
campaign_id: flags.campaign,
cpa_goal: money(flags['cpa-goal'], flags.currency),
default_bid_amount: money(flags['default-bid'], flags.currency),
end_time: startOfDayUtc(flags['end-time']),
name: flags.name,
pricing_model: flags['pricing-model'],
start_time: startOfDayUtc(flags['start-time'] ?? todayUtc()),
start_time: flags.automated ? undefined : startOfDayUtc(flags['start-time'] ?? todayUtc()),
status: flags.status,
}
await confirmMutation(
Expand Down
Loading
Loading