> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.qonversion.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Apple Ads via MCP

> Read Apple Search Ads performance and manage campaigns from an AI assistant, with the guardrails that keep it honest

<Note>
  Apple Ads is a **closed beta**. Accounts without it receive `404` from every tool on this page — the same answer as a nonexistent endpoint, by design. Ask support to enable it before wiring an assistant to it.
</Note>

The [MCP server](/docs/mcp-server) exposes the Apple Ads section of the dashboard as four tools: three that read and one family that changes live campaigns. This page covers what each one answers, and — more importantly — the handful of ways their output is easy to misread.

## The tools

| Tool                                                                      | Scope             | What it answers                                                                       |
| ------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------- |
| `get_apple_ads_report`                                                    | `analytics:read`  | Qonversion cohort outcomes joined with Apple spend, per campaign / ad group / keyword |
| `get_apple_ads_series`                                                    | `analytics:read`  | One metric, day by day — the chart behind that report                                 |
| `get_apple_ads_connection_status`                                         | `analytics:read`  | Whether the Apple authorization works, and what it currently allows                   |
| `set_apple_ads_status`, `set_apple_ads_bid`, `set_apple_ads_daily_budget` | `apple_ads:write` | Pause/enable an entity, move a bid, move a daily budget                               |

## Reading the report

`get_apple_ads_report` returns one row per entity at the requested `level`, each carrying a `qonversion` block (cohort outcomes), an `apple` block (spend and delivery), and derived ratios.

<Warning>
  **`avg_cpa` is not CAC.** `apple.avg_cpa` is Apple's cost per tap-through *install*. The cost of acquiring a **paying user** is `windows.<code>.cac`, and the two differ by the entire install→payer funnel. Quoting one as the other overstates efficiency by an order of magnitude on most accounts.
</Warning>

### Closed maturation windows

Row-level `revenue`/`roas` are cumulative **to date**: revenue keeps accruing while spend is bounded by the period, so they cannot compare cohorts of different ages. Request `windows` (`d0`, `d7`, `d30`, … `lifetime`) and every row gains a block computed only over install dates that have fully matured.

* `is_closed: false` means the numbers cover a closed **subrange** — `[closed_from, closed_through]`, not `[from, to]`. They are provisional, and the ratios can move in either direction. Say the window is still maturing and name `closed_through`.
* `d0` means **through the end of the install UTC day**, not the first 24 hours from the install moment.
* `lifetime` never closes, by definition.
* `null` is not zero. A missing ratio means "not available yet"; a `spend_closed: 0` is a measured zero.
* A date is only final once it is past both the window length plus the buffer **and** the restatement floor (`meta.windows.spend_snapshot.rewrite_days_effective`) — the store rewrites its most recent days, and no date is called final while that can still happen.

### Markers to check before quoting anything

`partial_sources` (the Apple side degraded — affected rows have `apple: null`), `meta.partial_scope` (how many campaigns lack spend), `meta.is_partial_period` (the range includes today), `meta.windows.spend_snapshot` (three distinct states: normal, still backfilling, read failed), and `meta.request.filters_applied` — which lists only the filters that were **recognised**, so check it before assuming a filter took effect.

## Reading the series

`get_apple_ads_series` plots one metric per UTC day.

<Warning>
  **Do not loop the report to build a time series.** The report's cohort metrics are cumulative to date from each install date; a series point is the value attributed to *that* day. The two answer different questions and return different numbers.
</Warning>

* `value: null` is **unknown** (outside the spend snapshot's coverage, missing from it, or an empty denominator), never zero.
* `is_cohort_metric: true` means the point belongs to the cohort **acquired** that day — revenue on 3 March is "what the people acquired on 3 March have paid to date", not revenue collected on 3 March.
* `filters_ignored` lists filters that were accepted and not applied (the entity-state filters have no meaning on a time axis). Non-empty means your numbers answer a wider question than you asked.

## Checking the connection

`get_apple_ads_connection_status` exists because an empty report and a refused change look identical whether the cause is "no data yet", "the Apple authorization expired" or "this Apple role cannot write". Call it before concluding anything from either.

It returns two **independent** readiness axes:

* `reporting` — whether performance data can flow.
* `campaign_management` — whether changes are allowed, with `can_write`, the per-`orgs[]` detail, and the exact Apple role names.

`connected` therefore does **not** imply write access; the ordinary case is a perfectly working integration whose Apple role only reads. Other states worth naming precisely:

| State                                | What it means                                                                                                                                                             |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `legacy_credentials`                 | Connected with an older customer key. Everything works; migrating is a recommendation, not a fault.                                                                       |
| `verifying`                          | The authorization is stored, Apple has not confirmed it yet. Not "connected and verified".                                                                                |
| `campaign_management.state: unknown` | Apple's daily snapshot of your roles has not landed, or is too old to be evidence. "Ask again later" — never "you are not allowed".                                       |
| Several `orgs[]`                     | Not a blocker and never a reason to reconnect. Management is available if at least one org allows it; the specific change is authorized against the org of that campaign. |

The response never contains a private key, client secret, token, authorization code or OAuth state — and the tool cannot start the sign-in flow. It returns `dashboard_url` instead, which is where a human completes it. **Never paste a `.p8` key or a client secret into a chat.**

## Changing campaigns

The write tools require the separate `apple_ads:write` scope **and** a dashboard user who is allowed to manage campaigns. A project Secret Key on its own is refused: that key authorizes the whole public API and tends to live in CI variables, while the right to spend money is granted per member in the dashboard.

<Warning>
  These tools change a live Apple Ads account and spend real money. Read the report first: you need the entity ids **and** the current value, because the current value is mandatory.
</Warning>

### The rules

* **`before_*` is required.** It is the optimistic lock (a `409` means the value changed under you — re-read the report, do not retry), the source an undo would use, and the sign of the spend change that the daily caps count.
* **One action per request**: a status, a bid, or a daily budget — never two.
* **Confirmation is asymmetric.** Pausing an entity or lowering a value applies immediately. Enabling one or raising a value is parked for human confirmation via [`confirm_destructive_action`](/docs/mcp-server#destructive-actions) — the token is addressed to the operator, not a cue for the assistant to confirm its own request.
* **`idempotency_key`** makes a retry the same attempt rather than a second change. Omit it and one is generated for this attempt; pass the same one back when retrying after an unknown outcome.
* **`dry_run: true`** changes nothing and returns `would_send`, the exact payload Apple would have received. Use it before any first-time or unusual change.
* **Amounts are decimal strings** in the **advertising account's** currency — never the currency you requested a report in, and never converted.

### Outcomes that must be handled differently

| Result        | What to do                                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `409`         | The value changed under you, or the idempotency key was reused with a different body. Re-read the report; do not retry the same body. |
| `422`         | The request cannot be executed. The message names what to change.                                                                     |
| `429`         | A rate limit or a daily cap on spend-increasing actions. `Retry-After` says how long; a daily cap does not clear within the minute.   |
| `502` / `504` | The outcome is **unknown** — the change may or may not have reached Apple. Read the report to see what happened. Never retry blindly. |

A success carries `action_id` (the audit row — name it when describing what you did). If it also carries `baseline_unverified: true`, there was no confirmed history for that entity, the ×3 raise ceiling could not be applied, and a human should look at the result.

### The bid you read back may be stale

Bids and budgets in a report row come from Apple's reporting metadata and can lag by hours — including behind a change you just made yourself. Confirm a write from the write's own response, not by re-reading the report a second later.

## Rate limits

Analytics tools are limited to 30 requests per minute, counted per project and per identical request. Over the limit you get a `429` with `Retry-After` — never a cached answer pretending to be fresh. Write actions have their own tighter limits plus daily caps on spend-increasing changes.
