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

# Apple Search Ads with appstore

> Keep an Apple Search Ads API key next to your App Store Connect key and call the Campaign Management API from the terminal or CI.

An `appstore` account can also hold an Apple Search Ads API key. The private key stays in your config, next to the App Store Connect key, and `appstore search-ads call` sends any Campaign Management API (v5) request with it. [Appeeky Spec](https://docs.appeeky.com/docs/spec-own-keys.md#apple-search-ads) reads Search Ads through the same account.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
appstore accounts search-ads <account> --client-id <id> --key-id <id> --private-key-path <pem> [--team-id <id>] [--org-id <id>]
appstore accounts search-ads <account> --remove
appstore search-ads orgs
appstore search-ads call <METHOD> <path> [jsonBody] [--org-id <id>]
```

This is local CLI only. The hosted MCP does not have Search Ads.

## Create the API user

1. In [Apple Search Ads](https://app.searchads.apple.com), invite a user with an API role (for example API Account Read Only) under **Account Settings → User Management**.

2. Sign in as that user and open **Account Settings → API**.

3. Make an EC key pair and upload the public key:

   ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
   openssl ecparam -genkey -name prime256v1 -noout -out searchads-private-key.pem
   openssl ec -in searchads-private-key.pem -pubout -out searchads-public-key.pem
   ```

4. Apple shows a client id, team id and key id. Keep the private key file outside the repo.

## Save it on an account

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
appstore accounts search-ads studio \
  --client-id SEARCHADS.… --team-id SEARCHADS.… --key-id … \
  --private-key-path ~/keys/searchads-private-key.pem
appstore --account studio search-ads orgs              # organizations the key reaches
appstore accounts search-ads studio --org-id 1234567   # pick one when there are several
appstore accounts search-ads studio --remove
```

The file must be an EC private key; a public key is refused. `accounts list` shows which accounts have Search Ads. The config keeps the path, not the key:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
"searchAds": {
  "clientId": "SEARCHADS.…",
  "teamId": "SEARCHADS.…",
  "keyId": "…",
  "privateKeyPath": "/Users/you/keys/searchads-private-key.pem",
  "orgId": "1234567"
}
```

You can also add both keys at once with `accounts add`:

| Flag | Meaning |
| - | - |
| `--search-ads-client-id <id>` | Client id |
| `--search-ads-team-id <id>` | Team id (defaults to the client id) |
| `--search-ads-key-id <id>` | Key id |
| `--search-ads-private-key-path <path>` | EC private key PEM |
| `--search-ads-org-id <id>` | Organization id |

## Environment variables

Without a saved account (in CI, for example):

| Variable | Meaning |
| - | - |
| `ASA_CLIENT_ID` | Client id |
| `ASA_TEAM_ID` | Team id (defaults to the client id) |
| `ASA_KEY_ID` | Key id |
| `ASA_PRIVATE_KEY` | PEM contents (`\n` escapes allowed) |
| `ASA_PRIVATE_KEY_PATH` | Path to the PEM |
| `ASA_ORG_ID` | Organization id |

As with the `ASC_*` variables, the environment wins over the default account. `--account` always uses that account's key. `ASA_ORG_ID` fills in the org when the account has none.

## Calls

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
appstore --account studio search-ads call GET '/api/v5/campaigns?limit=1000'
appstore --account studio search-ads call GET /api/v5/campaigns/123/adgroups/456/targetingkeywords
appstore --account studio search-ads call POST /api/v5/reports/campaigns @report.json
appstore --account studio search-ads call GET /api/v5/acls
```

* The CLI gets an access token and adds `X-AP-Context: orgId=…`. `/api/v5/acls` is sent without it.
* The org comes from `--org-id`, then the account (or `ASA_ORG_ID`), then the only org the key reaches. With several orgs and none set, the command fails and lists them.
* Reports and `…/find` queries are reads. Other `POST`, `PUT` and `DELETE` calls need `--confirm`; `--read-only` blocks them.
* On success, stdout is Apple's JSON body, for example `{"data": [...], "pagination": {...}, "error": null}`.

## Errors

On failure the exit code is 1 and stderr has one line, for example `Apple Search Ads 400: endTime is invalid (field: endTime) on POST /api/v5/reports/campaigns`. With `--json`, stdout also gets:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": {
    "code": "SEARCH_ADS_ERROR",
    "status": 400,
    "message": "Apple Search Ads 400: endTime is invalid (field: endTime) on POST /api/v5/reports/campaigns",
    "messageCode": "INVALID_INPUT",
    "field": "endTime",
    "method": "POST",
    "path": "/api/v5/reports/campaigns",
    "errors": [{ "messageCode": "INVALID_INPUT", "message": "endTime is invalid", "field": "endTime" }]
  }
}
```

Errors before the request (no key, bad path) use the same shape with `code` `AUTH_ERROR` or `VALIDATION_ERROR` and no `status`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.