# API keys
Source: https://docs.getadloop.com/cloud/api-keys
Create, restrict, reveal, and revoke the keys your AI tools use.
Every AI tool you connect authenticates with an API key (`alc_...`) from **Settings → API keys**. Your first key is created automatically during onboarding.
## Creating a key
Each key has a label (name the tool it's for; it helps when cleaning up later) and optional restrictions:
* **Read-only**: the server refuses every write tool for this key. The AI can analyze and report but never change anything, see [the safety model](/concepts/safety-model#read-only-keys).
* **Toolsets**: expose only selected tool groups, which shrinks the tool list your AI loads into context. See [Toolsets](/concepts/toolsets).
* **Client scoping** (agency plan): limit a key to specific clients. See [Clients](/cloud/clients).
Restrictions combine freely: a read-only, `ga4`-only key scoped to one client is a perfectly good reporting handout.
## Key values
The key value is shown once at creation and stored encrypted; **Reveal** shows it again later (keys from before this feature can't be revealed — rotate instead). Treat keys like passwords.
## Revoking
**Revoke** takes effect immediately; tools using that key lose access on their next call. Deleting a client (agency plan) revokes its scoped keys in the same step.
## Narrowing access
You can narrow a key's client set after creation ("Zugriff bearbeiten"), but never widen it: a credential must never gain access it didn't have when it was issued. Wider access takes a new key.
# Clients (agency plan)
Source: https://docs.getadloop.com/cloud/clients
One workspace, many client accounts, clean separation.
On the agency plan, each client is a **profile**: an Ads account + GA4 property pairing (optionally with GTM container, Search Console property, Merchant Center account, and [Reddit ad account](/integrations/reddit-ads)). Manage them under **Settings → Clients**.
## What profiles give you
* **Scoped API keys**: a key limited to specific clients can only reach those clients' accounts. The server refuses calls targeting anything else, with a message that names the key's allowed clients. Hand a client-scoped, read-only key to the client themselves for transparent reporting.
* **Per-client attribution**: usage and the change log are attributed to the client a call was for, so you can show any client exactly what changed in their account and when.
* **Per-call account selection**: your own tenant-wide key (agency owner) can address any client account directly in chat; scoped keys stay locked to their clients.
## MCC support
Connect your Manager Account once; client accounts under it are addressable without separate Google connections per client.
## Solo plan
Solo workspaces manage exactly one Ads + GA4 pairing, and every credential is locked to it. That's the plan working as designed; per-call account overrides are an agency feature.
# Plans and limits
Source: https://docs.getadloop.com/cloud/plans-and-limits
What counts as an operation, how much each plan includes, and what happens at the limit.
AdLoop Cloud measures one thing: **ad platform operations**, the calls your AI makes to Google Ads and Reddit Ads. Everything else (Analytics, Tag Manager, Search Console, Merchant Center, PageSpeed) is unmetered on every plan, including Free.
## Plans
| | Free | Pro | Agency |
| - | - | - | - |
| Ad platform operations per month | 250 | No limit | No limit |
| Ad platform operations per day | 100 | No limit in normal use (fair use: 2,000) | No limit in normal use (fair use: 5,000) |
| Analytics, Tag Manager, Search Console, Merchant Center, PageSpeed | Unmetered | Unmetered | Unmetered |
| Accounts | One profile: one ad account per platform + 1 GA4 property | One profile: one ad account per platform + 1 GA4 property | Unlimited [client profiles](/cloud/clients) |
| Writes with preview, dry run and budget cap | Yes | Yes | Yes |
| API keys | Yes | Yes | Also scoped to single clients |
| Credit card | Not needed | Required | Required |
Free has no time limit. Prices are on the [pricing page](https://getadloop.com/pricing); you book or change a plan under **Settings → Billing**.
The fair-use ceilings on Pro and Agency sit far above anything a real workspace has used. They exist so a runaway script stops somewhere.
## What counts as an operation
Every Google Ads or Reddit Ads tool call is one operation, reads and drafts alike. The two platforms count together, against the daily number and, on Free, the monthly one.
* **`confirm_and_apply`**, and a click on the [approval card](/concepts/safety-model#approval-cards-only-your-click-applies), counts toward the platform of the plan it applies. Applying a GA4 key event doesn't count.
* **Cross-channel tools** that query Google Ads (`analyze_campaign_conversions`, `landing_page_analysis`, `attribution_check`) count as Google Ads operations.
* **Not counted:** `health_check` and `list_accounts`, so checking that everything works never costs you operations.
* **Refused calls aren't counted.** A call that stops before reaching the ad platform (setup not finished, a read-only key, a toolset the key doesn't include, the limit itself) shows up as activity but costs nothing.
## The limits
* **Daily:** resets at **midnight UTC**.
* **Monthly (Free only):** 250 operations per calendar month, reset on the **1st (UTC)**. It comes on top of the daily limit, so a single day still stops at 100.
The dashboard shows today's usage and, on Free, this month's. You get an email when a workspace passes 80% of a limit (once per day for the daily one, once per month for the monthly one), so the limit is never the first sign.
At a limit, Google Ads and Reddit Ads tools are refused with a message that says which limit was reached and when it resets. Analytics, Tag Manager, Search Console, Merchant Center and PageSpeed keep working, and so do `health_check` and `list_accounts`.
The daily plan limits apply to every workspace since 15 October 2026. The monthly Free limit applies to workspaces created since 20 October 2026, and to every free workspace from 1 December 2026.
# Consent and data gaps
Source: https://docs.getadloop.com/concepts/consent-and-data
Why Ads clicks and GA4 sessions never match in the EU, and why that's not a bug.
The single most common false alarm in EU ad accounts: *"Google Ads shows 10 clicks but GA4 only 3 sessions — my tracking is broken!"* Usually it isn't. AdLoop's analysis tools understand this; here's the background.
## The mechanics
* **Google Ads counts every click.** No consent needed; a click is a click.
* **GA4 only records users who accepted analytics cookies.** Visitors who reject or ignore the consent banner are invisible to GA4.
* In EU traffic, 30–70% of users typically decline. A clicks-to-sessions ratio of 2:1 to 5:1 is **normal**, not broken tracking.
* Conversion counts are affected the same way: your true conversion rate is likely higher than GA4 reports.
## What AdLoop does about it
The cross-reference tools (`analyze_campaign_conversions`, `attribution_check`) factor consent gaps into their analysis and say so in their insights, so your AI doesn't misdiagnose normal EU consent behavior as a tracking bug. Genuine tracking issues (zero sessions across all traffic sources, missing events, paused GTM tags) are still flagged, and the three-way tracking audit pinpoints them.
## When to actually worry
* GA4 shows **zero** sessions from paid traffic while organic looks normal: check UTM parameters and GA4 filters.
* Events fire in the browser but never arrive in GA4: run the tracking audit (`audit_event_coverage`) across your website code, GTM container, and GA4.
* Sites with Google Consent Mode v2 send cookieless pings even without consent, which narrows the gap; if your numbers are closer than expected, that's likely why.
# The safety model
Source: https://docs.getadloop.com/concepts/safety-model
The AI proposes. Only you apply.
AdLoop's safety model exists because AI touching ad budgets is a reasonable thing to be nervous about. It is enforced **server-side**: even a client that ignores every instruction cannot skip it.
## Preview before everything
Every write tool (`draft_campaign`, `add_negative_keywords`, `pause_entity`, ...) returns a **preview** with a `plan_id` instead of changing anything. Your AI shows you the preview; nothing happens until you approve it: with a click on the approval card in apps that show one, otherwise through `confirm_and_apply` with that plan ID.
## Approval cards: only your click applies
In AI apps that support MCP Apps (Claude, ChatGPT, VS Code and others), the preview appears as an **AdLoop approval card** in the chat: what changes, any warnings, and **Test run**, **Apply** and **Discard** buttons. When you pause, enable or remove something in Google Ads, the card names it and the account next to the IDs, for example campaign “Brand DE” (1234567890) · Example GmbH (123-456-7890), so a wrong account stands out before you click. An ID that matches nothing in the account is refused before a card appears. Test run is the dry run below; Apply is enabled once it has passed.
In those apps, only the click applies a change:
* The buttons call a tool that the app keeps out of the AI's tool list, and AdLoop offers it only to apps that show cards.
* Every click carries a one-time code for that one preview. The code goes to the card, not into the AI's context, and the AI cannot compute it.
* If the AI calls `confirm_and_apply` with `dry_run=false` itself, AdLoop refuses (`APPROVE_ON_CARD`). A typed "yes, apply it" in the chat is not enough, and neither is "Always allow" in the app's permission settings.
* Changes that cannot be undone also need the "cannot be undone" box ticked on the card. The server checks it.
## Confirmation dialogs in apps without cards
Apps that don't show cards but support confirmation dialogs (MCP elicitation) get a dialog instead: when the AI calls `confirm_and_apply` with `dry_run=false`, AdLoop itself asks you in the app's dialog what will change and whether to apply it. Changes that cannot be undone need an extra "cannot be undone" tick. The AI cannot answer the dialog for you; if you decline or close it, nothing changes and the preview stays pending.
There is no dialog for dry runs, or for a plan that still needs its required dry run first. On AdLoop Cloud the dialog needs an app on the current MCP protocol (2026-07-28); apps on older protocol versions keep the chat flow there. Self-hosted, both work.
Apps with neither cards nor dialogs keep the chat flow: the AI shows the preview and calls `confirm_and_apply` after your explicit confirmation.
If the card or the dialog doesn't work in your app, the workspace owner can allow approval in chat as well, which skips both (Cloud: **Settings → Safety → Also allow approval in chat**; self-hosted: `safety.allow_chat_approval: true`). Combined with "Always allow" for `confirm_and_apply` in your AI app, the AI could then apply changes without any click, so leave it off where the card works.
## Dry-run by default
`confirm_and_apply` defaults to `dry_run=true`, which checks the change without applying it:
* **Google Ads**: the exact change is sent to Google with `validate_only`. Google checks it the way it would check the real thing (field values, policy, whether the campaign or ad group exists) and executes nothing. If a change builds on something an earlier step would create, such as keywords for a new negative keyword list, the later step can't be checked yet and is reported as skipped.
* **Reddit Ads**: Reddit has no validate-only mode, so the dry run re-reads the target and re-checks the safety caps; nothing is sent.
A failed dry run (`DRY_RUN_FAILED`) carries the platform's error and means the real apply would fail the same way. Applying for real requires an explicit `dry_run=false`, and two settings can restrict it further:
* **`require_dry_run`**: all real applies are refused until you disable this (self-hosted: `config.yaml`; Cloud: dashboard safety settings)
* **Two-phase apply**: a plan must pass a dry run before it can be applied for real (always on for Cloud tenants). A failed dry run doesn't count, so a change the platform rejects can't be applied.
## Created paused
New campaigns and ads are always created **PAUSED**, on Google Ads and Reddit Ads alike, and so are new Reddit ad groups. A new Google Ads ad group is created enabled, but it can't serve until one of its ads is enabled. You review everything in the platform's UI (or by chat) and enable it explicitly, via `enable_entity` / `enable_reddit_entity`, which itself goes through preview and confirm.
## Budget caps
`max_daily_budget` caps every budget AdLoop can set. Drafts above the cap are rejected at draft time, before anything reaches the ad platform. Cloud additionally enforces [daily operation limits](/cloud/plans-and-limits) per workspace.
## Bid caps
`max_bid_increase_pct` (default 100%) limits how far a single change can raise a bid: a Google Ads ad group's manual CPC, a Maximize Clicks CPC ceiling, and Reddit ad group bids. Raising €2.00 to €5.00 is refused at draft time at the default. A first-time ceiling has nothing to compare against and isn't capped.
## Guardrails against classic money-wasters
* **Broad Match + Manual CPC is refused.** Broad match keywords without smart bidding are the most common cause of wasted spend. Creating a campaign with that combination is refused; adding broad keywords to an existing manual-CPC ad group is flagged in the preview.
* **Campaigns require geo and language targeting.** Untargeted campaigns serve globally and waste budget; drafts without targeting are rejected.
* **URLs are validated.** Ads and sitelinks pointing at unreachable pages are rejected at draft time.
## Read-only keys
A read-only API key (Cloud) refuses every write tool at the server. It's the only way to get "analyze but never change": Google's OAuth scopes have no read-only mode for Ads. Ideal for reporting access and cautious starts.
## Audit log
Every write operation, including dry runs, is logged: self-hosted to `~/.adloop/audit.log`, Cloud to the change log (Protokoll) in your dashboard, attributable per client for agencies.
## Destructive operations
`remove_entity` is irreversible and triggers double confirmation. Prefer `pause_entity` unless you explicitly want permanent removal.
# Toolsets
Source: https://docs.getadloop.com/concepts/toolsets
Expose only the tools you use and cut your AI's context cost.
Most MCP clients load **every tool schema into the model's context at the start of every conversation**. With AdLoop's full catalog that is a noticeable share of the context window, paid before you type a word. Toolsets let you expose a subset.
## The toolsets
| Toolset | Covers |
| - | - |
| `ads` | Google Ads reads, writes, and planning (Keyword Planner) |
| `ga4` | Google Analytics reports, realtime, key events |
| `tracking` | Cross-channel attribution and tracking code generation |
| `gtm` | Google Tag Manager audits and reads |
| `gsc` | Search Console reads |
| `web` | PageSpeed / Core Web Vitals |
| `merchant` | Merchant Center feed health |
| `reddit` | Reddit Ads reads, writes, and planning |
### Narrowing Google Ads
You can also pick part of Google Ads instead of the whole `ads` toolset (on AdLoop Cloud: tick Google Ads when creating a key and choose its scope):
| Subset | Covers |
| - | - |
| `ads:read` | Ads reports, GAQL, and the Ads/Analytics cross-checks (`attribution_check`, `analyze_campaign_conversions`, `landing_page_analysis`) |
| `ads:write` | Ads drafts and changes. Turns on `ads:read` too, because a draft needs the reads first (IDs, bidding strategy) |
| `ads:plan` | Keyword ideas and budget forecasts (`discover_keywords`, `estimate_budget`) |
`ads:read,ga4` suits audits and diagnosis; `ads:plan` alone suits keyword research.
`health_check` and `confirm_and_apply` are always included, whatever you select, so diagnostics and the [safety gate](/concepts/safety-model) can never be filtered away.
## What it saves
Measured against the live tools/list: `ads:read,ga4` costs about 40% of `ads,ga4`, and `ads:plan` alone about 5% of the full catalog. Toolsets are per **consumer**, not per install: one AdLoop setup can serve a trimmed Cursor and a full-catalog Claude Code side by side.
## AdLoop Cloud: per API key
Pick toolsets when creating a key under **Settings → API keys**. The server then trims that key's advertised tool list and refuses calls outside the selection, with a clear message pointing back to the dashboard. Existing keys are unrestricted until you say otherwise.
## Self-hosted: environment variable
```json theme={null}
"env": { "ADLOOP_TOOLSETS": "ads:read,ga4" }
```
Set it in your MCP client's configuration (the `adloop init` wizard offers this and writes it into the snippets). Unset means the full catalog; unknown names fail at startup listing the valid ones.
# Connect ChatGPT
Source: https://docs.getadloop.com/connect/chatgpt
Add AdLoop Cloud to ChatGPT via developer mode.
ChatGPT connects to **AdLoop Cloud** (remote MCP servers) as a custom **plugin**. Custom plugins are available on paid ChatGPT plans and need developer mode, which moved to the security settings (checked 2026-09-16).
**Settings → Security and login → Developer mode** (marked "elevated risk": it allows unverified connectors). Once per account.
In the ChatGPT sidebar click **Plugins** (or go to [chatgpt.com/plugins](https://chatgpt.com/plugins)), then the **+** button at the top right: **New Plugin**.
Name it **AdLoop**. Connection: **Server URL** with
```
https://mcp.getadloop.com/mcp
```
Authentication: **OAuth** (leave the advanced settings alone; ChatGPT discovers them from the server). Tick *I understand and want to continue* under the custom-server warning and click **Create**.
ChatGPT shows an "Add AdLoop to ChatGPT" card: click **Sign in with AdLoop**. AdLoop's consent page opens in a new tab; sign in to your workspace and choose **Allow access**. The token is issued to chatgpt.com only and the tab closes by itself.
Click **+** next to the message box and pick AdLoop (or type its name). Then ask: *"Run the AdLoop health check."*
## After an AdLoop update
ChatGPT remembers AdLoop's tool list from when you connected it. When AdLoop adds tools, they only show up after a refresh: open AdLoop on the **Plugins** page and refresh it there. If your version offers no refresh, delete the plugin and create it again with the same URL; you sign in once more.
Not sure whether yours is current? Ask: *"Run the AdLoop health check."* It lists the tools AdLoop offers you and says so when your app is showing an older list.
## Agencies
A ChatGPT connection is workspace-wide, not tied to one of your clients. Name the client in your request ("for Kersten Coating, how did last week go?") or let the assistant ask; it gets your client list with their account ids when a request doesn't say.
## OpenAI Agent Builder
In a workspace agent (Business plans), add AdLoop as an **MCP** tool with the same server URL and OAuth authentication; the consent flow is identical.
## Notes
* ChatGPT moves this around: before September 2026 it lived under Settings → Connectors → Advanced. If the Plugins page or the developer-mode switch look different, search settings for "plugin", "developer" or "MCP".
* ChatGPT asks before a plugin's write actions (Settings → Plugins → Permissions); AdLoop previews and dry-runs every write on top of that.
* Every write goes through the [preview and confirm flow](/concepts/safety-model) regardless of client, so an over-eager agent can't apply anything without you.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect claude.ai
Source: https://docs.getadloop.com/connect/claude-ai
Add AdLoop Cloud as a custom connector in claude.ai.
claude.ai connects to **AdLoop Cloud** only (remote connectors can't reach a server on your laptop). A connector you add is available in claude.ai, Claude Desktop and the Claude mobile apps. The Free plan allows one custom connector; on Team and Enterprise plans an Owner adds it for the organization (checked 2026-10-10). For the self-hosted edition, use [Claude Code](/connect/claude-code) or [Cursor](/connect/cursor).
Free, Pro and Max: **Customize → Connectors → Add custom connector**. Team and Enterprise: **Organization settings → Connectors → Add → Custom → Web**.
```
https://mcp.getadloop.com/mcp
```
claude.ai redirects you to AdLoop's consent page, where you sign in with your AdLoop Cloud account and approve the connection. No API key is pasted anywhere; the connector uses OAuth.
Ask Claude: *"Run the AdLoop health check."* You should see your GA4 and Ads connection status.
## After an AdLoop update
claude.ai (and with it Claude Desktop and the mobile apps) remembers AdLoop's tool list from when you connected it. When AdLoop adds tools, they only show up after a refresh: open **Settings → Connectors → AdLoop** and refresh the connector. Until then Claude may say a tool "isn't exposed in this session". The same applies after you add a [toolset](/concepts/toolsets) or an integration such as [Reddit Ads](/integrations/reddit-ads).
Not sure whether yours is current? Ask: *"Run the AdLoop health check."* It lists the tools AdLoop offers you and says so when your app is showing an older list.
## Notes
* Connector access is workspace-wide for your tenant. Per-key restrictions (read-only, toolsets, client scoping) apply to API-key connections; the OAuth connector uses your tenant's full configuration.
* Every write still goes through the [preview and confirm flow](/concepts/safety-model), regardless of client.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Claude Code
Source: https://docs.getadloop.com/connect/claude-code
Add AdLoop to Claude Code, Cloud or self-hosted.
## AdLoop Cloud
Use your API key from **Settings → API keys** in the [dashboard](https://getadloop.com):
```bash theme={null}
claude mcp add --transport http adloop https://mcp.getadloop.com/mcp \
--header "Authorization: Bearer alc_YOUR_KEY"
```
Or sign in with AdLoop instead of using a key: add the server without the header, then run `/mcp` in Claude Code, pick AdLoop and choose **Authenticate**. Your browser opens AdLoop's consent page.
```bash theme={null}
claude mcp add --transport http adloop https://mcp.getadloop.com/mcp
```
## Self-hosted
After running `adloop init`:
```bash theme={null}
claude mcp add --transport stdio adloop -- python -m adloop
```
To expose only some [toolsets](/concepts/toolsets) (smaller tool list, less context per conversation):
```bash theme={null}
claude mcp add --transport stdio adloop --env ADLOOP_TOOLSETS=ads,ga4 -- python -m adloop
```
Or in your project's `.mcp.json`:
```json theme={null}
{
"mcpServers": {
"adloop": {
"command": "python",
"args": ["-m", "adloop"],
"env": { "ADLOOP_TOOLSETS": "ads,ga4" }
}
}
}
```
## Orchestration rules
For self-hosted setups, `adloop install-rules` installs orchestration rules and slash commands into `~/.claude/` so Claude knows the safety patterns, GAQL reference, and workflows. Cloud users get the essential rules delivered through the server itself.
## Verify
```
> Run the AdLoop health check.
```
You should see OAuth, GA4, and Ads status. If something fails, see [Troubleshooting](/troubleshooting).
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect OpenAI Codex
Source: https://docs.getadloop.com/connect/codex
Add AdLoop Cloud to the Codex CLI, IDE extension and desktop app.
The Codex CLI, the IDE extension and the desktop app share one configuration, `~/.codex/config.toml`, so setting AdLoop up once covers all three (checked 2026-10-10). Codex in ChatGPT on the web doesn't read that file; it uses plugins instead, see [ChatGPT](/connect/chatgpt).
## Sign in with AdLoop (OAuth)
```bash theme={null}
codex mcp add adloop --url https://mcp.getadloop.com/mcp
```
Codex detects that AdLoop supports OAuth and starts the sign-in straight away: your browser opens AdLoop's consent page, you choose **Allow access**, and the browser hands the token back to Codex. If it expires or you want to switch workspaces later:
```bash theme={null}
codex mcp login adloop
```
## API key
Keep the key in an environment variable rather than in the config file:
```bash theme={null}
export ADLOOP_API_KEY=alc_YOUR_KEY
codex mcp add adloop --url https://mcp.getadloop.com/mcp --bearer-token-env-var ADLOOP_API_KEY
```
That writes this to `~/.codex/config.toml`, which you can also edit by hand:
```toml theme={null}
[mcp_servers.adloop]
url = "https://mcp.getadloop.com/mcp"
bearer_token_env_var = "ADLOOP_API_KEY"
```
Codex sends the variable's value as `Authorization: Bearer …`. A key can be [restricted](/cloud/api-keys) to read-only, single clients or chosen toolsets.
## IDE extension
In the Codex panel: gear menu → **MCP servers** → **Add server**, choose **Streamable HTTP**, enter `https://mcp.getadloop.com/mcp`, save and **restart the extension**. Click **Authenticate** next to AdLoop to sign in. A server you added with the CLI shows up here too.
## Verify
```bash theme={null}
codex mcp list
```
Or type `/mcp` in a Codex session. Then ask: *"Run the AdLoop health check."*
## Notes
* AdLoop answers some reports in more than ten seconds. If Codex gives up on long calls, raise `tool_timeout_sec` under `[mcp_servers.adloop]` (default 60).
* Every write goes through the [preview and confirm flow](/concepts/safety-model), so an agent running unattended can't apply a change without you.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Cursor
Source: https://docs.getadloop.com/connect/cursor
Add AdLoop to Cursor via mcp.json.
## AdLoop Cloud
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global), with your API key from **Settings → API keys**:
```json theme={null}
{
"mcpServers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp",
"headers": {
"Authorization": "Bearer alc_YOUR_KEY"
}
}
}
}
```
To sign in with AdLoop instead of using a key, leave out `headers`. Cursor then offers a login next to AdLoop in its MCP settings, which opens AdLoop's consent page. Cursor receives the sign-in through an app link rather than a website, so the consent page asks you to confirm that destination before **Allow access** works.
## Self-hosted
```json theme={null}
{
"mcpServers": {
"adloop": {
"command": "python",
"args": ["-m", "adloop"],
"env": { "ADLOOP_TOOLSETS": "ads,ga4" }
}
}
}
```
The `env` block is optional; leave it out for the full catalog. See [Toolsets](/concepts/toolsets).
Cursor warns when many MCP tools are active at once. If you run several MCP servers, a restricted AdLoop key (or `ADLOOP_TOOLSETS`) keeps the tool list lean. Copy `.cursor/rules/adloop.mdc` from the [repository](https://github.com/kLOsk/adloop) into your project so Cursor knows the orchestration and safety patterns.
## Verify
Ask Cursor's agent to run the AdLoop health check. If something fails, see [Troubleshooting](/troubleshooting).
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Devin Desktop (Windsurf)
Source: https://docs.getadloop.com/connect/devin-desktop
Add AdLoop Cloud to Devin Desktop, formerly Windsurf: Devin Local, the Devin CLI and the legacy Cascade agent.
Windsurf became **Devin Desktop** in June 2026. New agent tabs use **Devin Local**; the older **Cascade** agent reads a separate MCP list. Devin Local and the Devin CLI share one configuration (checked 2026-10-10).
## Devin Local and Devin CLI
The quickest way is the CLI:
```bash theme={null}
devin mcp add -s user adloop https://mcp.getadloop.com/mcp
devin mcp login adloop
```
`login` opens AdLoop's consent page; choose **Allow access**. Leave out `-s user` to add AdLoop to the current project only.
Or edit `~/.config/devin/mcp_config.json` (Windows: `%APPDATA%\devin\mcp_config.json`; per project: `.devin/mcp_config.json`):
```json theme={null}
{
"mcpServers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp",
"transport": "http"
}
}
}
```
If AdLoop shows **Needs auth** in the MCP list, click **Authenticate**.
```json theme={null}
{
"mcpServers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer ${env:ADLOOP_API_KEY}" }
}
}
}
```
Set `ADLOOP_API_KEY` to a key from **Settings → API keys** in the [dashboard](https://getadloop.com) before starting Devin Desktop. To keep a key out of a shared project file, put the entry in `.devin/mcp_config.local.json` instead.
Check with `devin mcp list`, then ask: *"Run the AdLoop health check."*
## Legacy Cascade agent
Cascade reads `~/.config/devin/mcp_config.json` too, but expects `serverUrl` (Windsurf versions before the rename used `~/.codeium/windsurf/mcp_config.json`):
```json theme={null}
{
"mcpServers": {
"adloop": {
"serverUrl": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer ${env:ADLOOP_API_KEY}" }
}
}
}
```
Open the file from the Cascade panel: **⋯** → MCPs → **Open MCP config file**. The MCPs section then lists AdLoop with its tools and an on/off switch.
## Notes
* Cascade can see at most 100 tools across all servers. If you run several, use an AdLoop key limited to a few [toolsets](/concepts/toolsets).
* On team plans, once an admin allowlists any MCP server, all others are blocked. The allowlist entry must be named exactly `adloop`, like the config key.
* MCP tools ask for your approval by default, and every AdLoop write goes through its own [preview and confirm flow](/concepts/safety-model) on top.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Gemini
Source: https://docs.getadloop.com/connect/gemini
AdLoop Cloud in the Gemini app, Antigravity and Gemini CLI.
Google offers three ways in, and they changed in 2026 (checked 2026-10-10):
* **Gemini app** (gemini.google.com): custom apps, for personal Google accounts in the US.
* **Antigravity** (CLI and IDE): the successor of Gemini CLI for personal Google accounts since June 2026.
* **Gemini CLI**: still available with Gemini Code Assist Standard or Enterprise, or with a paid Gemini API key.
## Gemini app
On gemini.google.com: **Settings → Connected Apps**, under *Custom apps* click **Add a custom app** and enter:
```
https://mcp.getadloop.com/mcp
```
Click **Next**. Leave the advanced credentials empty; Gemini registers itself with AdLoop.
AdLoop's consent page opens. Sign in to your workspace and choose **Allow access**.
In a chat, type `@`, pick AdLoop, and ask: *"Run the AdLoop health check."* Once set up on the web, it works in the Gemini mobile app too.
Google currently limits custom apps to personal accounts of adults in the US with Keep Activity turned on, and to English. Work and school accounts can't add them yet.
## Antigravity
Antigravity reads `~/.gemini/config/mcp_config.json` (per workspace: `.agents/mcp_config.json`). Note the key `serverUrl`; Antigravity doesn't accept Gemini CLI's `httpUrl`.
```json theme={null}
{
"mcpServers": {
"adloop": {
"serverUrl": "https://mcp.getadloop.com/mcp"
}
}
}
```
Then open the agent settings (`⌘,`) → **Customizations** and click **Authenticate** next to AdLoop. Approve on AdLoop's consent page, copy the code it shows back into Antigravity and click **Submit**.
```json theme={null}
{
"mcpServers": {
"adloop": {
"serverUrl": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer alc_YOUR_KEY" }
}
}
}
```
In the CLI (`agy`), `/mcp` opens the MCP manager with AdLoop's status. If you used AdLoop in Gemini CLI before, `agy plugin import gemini` carries the server entry over.
## Gemini CLI
```bash theme={null}
gemini mcp add --transport http -s user adloop https://mcp.getadloop.com/mcp
```
Then sign in from inside a session:
```
/mcp auth adloop
```
Your browser opens AdLoop's consent page; choose **Allow access**. This needs a browser on the same machine. On a server or over SSH, use an API key instead:
```bash theme={null}
gemini mcp add --transport http -s user \
-H "Authorization: Bearer alc_YOUR_KEY" adloop https://mcp.getadloop.com/mcp
```
Both write to `~/.gemini/settings.json`, where the server sits under `mcpServers` with the key `httpUrl`. Check with `gemini mcp list` or `/mcp`.
The [AdLoop extension](https://github.com/kLOsk/adloop-plugins) adds the server and AdLoop's usage rules in one step:
```bash theme={null}
gemini extensions install https://github.com/kLOsk/adloop-plugins
```
## After an AdLoop update
The Gemini app remembers AdLoop's tool list from when you connected it. When AdLoop adds tools, they only show up after a refresh: under **Settings → Connected Apps**, disconnect AdLoop and connect it again. Antigravity and Gemini CLI read the list when they start, so a restart (or `/mcp` in the CLI) is enough there.
Not sure whether yours is current? Ask: *"Run the AdLoop health check."* It lists the tools AdLoop offers you and says so when your app is showing an older list.
## Notes
* A connection made through OAuth uses your whole workspace. An API key can be [restricted](/cloud/api-keys) to read-only, single clients or chosen toolsets.
* Every write goes through the [preview and confirm flow](/concepts/safety-model), whichever Gemini product you use.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Mistral Vibe (Le Chat)
Source: https://docs.getadloop.com/connect/mistral-vibe
Add AdLoop Cloud to Mistral Vibe, formerly Le Chat, as a custom MCP connector.
Le Chat is now called **Vibe**, at the same address, chat.mistral.ai (checked 2026-10-10). Custom MCP connectors work on every plan, including Free. On Team and Enterprise plans an admin adds them for the organization.
In the sidebar click **Connectors**, then **+ Add Connector** and the **Custom MCP Connector** tab.
Connector name: `AdLoop` (no spaces or special characters). Server URL:
```
https://mcp.getadloop.com/mcp
```
Click **Connect**. Vibe detects that AdLoop uses OAuth and registers itself.
AdLoop's consent page opens. Sign in to your workspace and choose **Allow access**.
Click **+** (or type `/`) → **Tools** and turn AdLoop on. Then ask: *"Run the AdLoop health check."*
## After an AdLoop update
Vibe remembers AdLoop's tool list from when you connected it. When AdLoop adds tools, they only show up after a refresh: open **Connectors → My Connectors → AdLoop** and reconnect it. If the new tools still don't appear, remove the connector and add it again.
Not sure whether yours is current? Ask: *"Run the AdLoop health check."* It lists the tools AdLoop offers you and says so when your app is showing an older list.
## Notes
* Under **Connectors → My Connectors → AdLoop → Functions** you can set individual read tools to *Always allow*. Leave writes on ask: AdLoop shows a [preview](/concepts/safety-model) before anything changes, and you approve it in the chat.
* If you want your AI provider in Europe as well: Mistral is based in France, and AdLoop's MCP server runs in Germany. See [Security and privacy](https://getadloop.com/security) for where AdLoop processes data.
* Agencies: the OAuth connection covers the whole workspace, so name the client in your request ("for Kersten Coating, how did last week go?").
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# More clients
Source: https://docs.getadloop.com/connect/more-clients
Cline, Warp, JetBrains Junie, JetBrains AI Assistant, and any other MCP client.
Any client that can add a remote MCP server over Streamable HTTP works with AdLoop Cloud. It needs two things:
* **URL:** `https://mcp.getadloop.com/mcp`
* **Sign-in:** either OAuth (the client discovers AdLoop's sign-in by itself and opens the consent page), or the header `Authorization: Bearer alc_YOUR_KEY` with a key from **Settings → API keys** in the [dashboard](https://getadloop.com).
The setups below were checked on 2026-10-10.
## Cline
Open the **MCP Servers** icon → **Remote Servers**, enter `adloop` and the URL, choose **Streamable HTTP** and click **Add Server**. Then click **Authenticate** to sign in. Or with an API key in `cline_mcp_settings.json`:
```json theme={null}
{
"mcpServers": {
"adloop": {
"type": "streamableHttp",
"url": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer alc_YOUR_KEY" }
}
}
}
```
Keep `"type": "streamableHttp"`: without it Cline assumes the older SSE transport.
## Warp
**Settings → Agents → MCP servers → + Add** → *Streamable HTTP or SSE Server (URL)*, and paste:
```json theme={null}
{
"adloop": {
"url": "https://mcp.getadloop.com/mcp"
}
}
```
Warp opens the sign-in in your browser when the server starts. For Warp's cloud agents, which can't sign in through a browser, add the API key instead: `"headers": { "Authorization": "Bearer alc_YOUR_KEY" }`.
## JetBrains Junie
Add AdLoop to `~/.junie/mcp/mcp.json` (all projects) or `.junie/mcp/mcp.json` (one project), or under **Tools → Junie → MCP Settings**:
```json theme={null}
{
"mcpServers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp"
}
}
}
```
Run `/mcp`, choose AdLoop and **Authorize** to sign in in your browser. AdLoop shows as *Active* once connected.
## JetBrains AI Assistant
**Settings → Tools → AI Assistant → Model Context Protocol (MCP) → Add**, and paste:
```json theme={null}
{
"mcpServers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer alc_YOUR_KEY" }
}
}
}
```
The *Status* column shows whether AdLoop is connected; click it to see the tools.
## Claude Desktop
Claude Desktop uses the connectors of your Claude account, so set AdLoop up once as described in [claude.ai](/connect/claude-ai). The local `claude_desktop_config.json` is for servers on your own machine and isn't needed.
If you also run the self-hosted edition in Claude Desktop, switch one of the two off in the chat's connector menu. Both offer tools with the same names, and Claude may pick the wrong one.
## Your client isn't listed?
If it supports remote MCP servers, use the URL and one of the two sign-ins above. Then ask: *"Run the AdLoop health check."* If sign-in fails, try the API key header; if the client only supports local (stdio) servers, use the [self-hosted edition](/quickstart/self-hosted) instead.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect your AI
Source: https://docs.getadloop.com/connect/overview
AdLoop Cloud works with any AI app that speaks MCP. Pick yours.
AdLoop Cloud is a remote MCP server at:
```
https://mcp.getadloop.com/mcp
```
Any AI app that can add a remote MCP server can use it. There are two ways to sign in:
* **Sign in with AdLoop (OAuth).** The app opens AdLoop's consent page, you approve, done. Nothing to copy. The connection uses your whole workspace.
* **API key.** You paste a key from **Settings → API keys** in the [dashboard](https://getadloop.com) into the app's config, sent as `Authorization: Bearer alc_…`. Keys can be [restricted](/cloud/api-keys): read-only, single clients, chosen toolsets.
Every write goes through the same [preview and confirm flow](/concepts/safety-model), whichever app you use.
## Chat apps
Custom connector, also in Claude Desktop and mobile. OAuth.
Custom plugin in developer mode, paid plans. OAuth.
Custom MCP connector, free plan included. OAuth.
Custom remote connector on Pro, Max and Enterprise. OAuth.
Gemini app custom apps (US), Antigravity and Gemini CLI.
## Coding agents and editors
One command, API key or OAuth.
`mcp.json`, API key or OAuth.
CLI, IDE extension and desktop app share one config.
Agent mode, `mcp.json`, API key or OAuth.
Devin Local and the legacy Cascade agent.
`context_servers` in settings, API key or OAuth.
Cline, Warp, JetBrains Junie and AI Assistant.
## Which sign-in to use
| | OAuth (sign in with AdLoop) | API key |
| - | - | - |
| Setup | Click through a consent page | Copy a key into a config file |
| Scope | Whole workspace | Restrictable per key (read-only, clients, toolsets) |
| Agencies | Name the client in your request | Bind the key to one client |
| Revoke | Settings → API keys → Connected AI apps | Settings → API keys |
Chat apps in the browser only offer OAuth. Coding agents offer both; an API key is handy when you want a read-only or client-scoped connection, or on a machine without a browser.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Perplexity
Source: https://docs.getadloop.com/connect/perplexity
Add AdLoop Cloud to Perplexity as a custom remote connector.
Custom connectors need a **Pro, Max or Enterprise** plan. On Enterprise, an admin first turns on *Allow members to add custom connectors* under Enterprise settings → Connectors; it's off by default (checked 2026-10-10).
In Perplexity: **Account settings → Connectors → + Custom connector** and choose **Remote**.
Name: `AdLoop`. MCP Server URL:
```
https://mcp.getadloop.com/mcp
```
Authentication: **OAuth**. Transport: **Streamable HTTP**. Leave client ID and secret empty; Perplexity registers itself with AdLoop. Tick the acknowledgement and click **Add**.
Click the AdLoop card to sign in. AdLoop's consent page opens; sign in to your workspace and choose **Allow access**.
In the composer, turn AdLoop on in the connector picker (or type `@`), and make sure the search focus isn't set to web only. Then ask: *"Run the AdLoop health check."*
## After an AdLoop update
Perplexity remembers AdLoop's tool list from when you connected it. When AdLoop adds tools, they only show up after a refresh: remove the AdLoop connector under **Account settings → Connectors** and add it again; Perplexity has no refresh button for custom connectors.
Not sure whether yours is current? Ask: *"Run the AdLoop health check."* It lists the tools AdLoop offers you and says so when your app is showing an older list.
## Notes
* Enterprise admins can share the connector with the whole organization and choose whether members sign in individually.
* Every write goes through the [preview and confirm flow](/concepts/safety-model): Perplexity can draft a change, but nothing is applied until you confirm it.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect VS Code (GitHub Copilot)
Source: https://docs.getadloop.com/connect/vscode
Use AdLoop Cloud in GitHub Copilot's agent mode.
VS Code runs MCP servers in Copilot's **agent mode**. If your GitHub organization manages Copilot, an admin may have to allow MCP servers first (checked 2026-10-10).
## Add the server
Run **MCP: Add Server** from the Command Palette (`⇧⌘P` / `Ctrl+Shift+P`), choose **HTTP**, enter `https://mcp.getadloop.com/mcp`, name it `adloop` and pick where to save it. Or write the config yourself in `.vscode/mcp.json` (one project) or your user `mcp.json` (**MCP: Open User Configuration**, all projects). Newer VS Code versions also offer a shared `.mcp.json`; the format below works in the VS Code files and supports the secure key prompt:
```json theme={null}
{
"servers": {
"adloop": {
"type": "http",
"url": "https://mcp.getadloop.com/mcp"
}
}
}
```
On first use VS Code opens AdLoop's consent page in your browser; choose **Allow access**. The account then appears in VS Code's **Accounts** menu.
VS Code asks for the key once and stores it securely, so it never sits in the file:
```json theme={null}
{
"inputs": [
{
"type": "promptString",
"id": "adloop-key",
"description": "AdLoop API key",
"password": true
}
],
"servers": {
"adloop": {
"type": "http",
"url": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer ${input:adloop-key}" }
}
}
}
```
Create the key under **Settings → API keys** in the [dashboard](https://getadloop.com).
## Verify
Open Copilot Chat, switch to **Agent**, and check that AdLoop appears under **Configure Tools**. **MCP: List Servers** shows its status and output. Then ask: *"Run the AdLoop health check."*
## Notes
* Copilot sends at most 128 tools per request across all servers. If you run several servers, deselect what you don't need under **Configure Tools**, or use an AdLoop key limited to a few [toolsets](/concepts/toolsets).
* To sign in to a different workspace, run **Authentication: Remove Dynamic Authentication Providers**, then start the server again.
* VS Code shows a trust prompt the first time a server starts and after its config changes.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# Connect Zed
Source: https://docs.getadloop.com/connect/zed
Use AdLoop Cloud in Zed's agent panel.
## Add the server
In Zed: **Settings → AI → MCP Servers → Add Server → Add Remote Server**. Or run **zed: open settings file** and add AdLoop under `context_servers` (user settings in `~/.config/zed/settings.json`, Windows `%APPDATA%\Zed\settings.json`; per project `.zed/settings.json`):
```json theme={null}
{
"context_servers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp"
}
}
}
```
Zed shows a warning with an **Authenticate** button next to AdLoop. Click it, choose **Allow access** on AdLoop's consent page, and Zed keeps the token in your system keychain and refreshes it by itself.
```json theme={null}
{
"context_servers": {
"adloop": {
"url": "https://mcp.getadloop.com/mcp",
"headers": { "Authorization": "Bearer alc_YOUR_KEY" }
}
}
}
```
Zed doesn't read environment variables here, so the key sits in the settings file: use your user settings, not a project file you commit. Create the key under **Settings → API keys** in the [dashboard](https://getadloop.com).
## Verify
A green dot next to AdLoop in the MCP Servers list means it's connected ("Server is active"). Open the agent panel and ask: *"Run the AdLoop health check."*
## Notes
* Zed asks before each tool call unless you change `agent.tool_permissions`; you can allow AdLoop's read tools individually as `mcp:adloop:`.
* Every write goes through AdLoop's [preview and confirm flow](/concepts/safety-model) in addition to Zed's own prompt.
**Your AI can read these docs too.** Add `https://docs.getadloop.com/mcp` as an additional MCP server and your assistant can search this documentation while it works: setup answers, troubleshooting, and tool guidance on demand.
# AdLoop Documentation
Source: https://docs.getadloop.com/index
Google Ads, Reddit Ads, Analytics, Tag Manager, and Search Console in your AI assistant, with previews before every change.
AdLoop connects your Google marketing stack, and optionally [Reddit Ads](/integrations/reddit-ads), to the AI assistant you already use: Claude, ChatGPT, Cursor, or Gemini. Analyze campaigns, hunt wasted spend, build ads by chat. Every change is previewed and dry-run first; nothing goes live without your approval.
Both editions run the same tools with the same safety model. The difference is who handles the plumbing.
Connect your Google account in two clicks. No Google Cloud project, no API access application, no OAuth verification wait. EU-hosted, GDPR-first, with a free plan that needs no credit card.
MIT-licensed MCP server on your own machine with your own Google Cloud project. The `adloop init` wizard walks you through setup in about five minutes.
## What you can do
* **Analyze**: campaign performance, search terms, quality scores, PMax insights, cross-referenced Ads and GA4 data that answers "what's my real cost per signup?"
* **Build**: complete campaigns by chat, on Google Ads and Reddit Ads. Keywords, ads, sitelinks, and budgets drafted as previews, created paused, enabled only by you.
* **Clean up**: wasted spend hunts, negative keyword workflows, three-way tracking audits across your website code, GTM container, and GA4.
* **Stay safe**: previews with plan IDs, dry-run by default, budget caps, an audit log, and read-only keys. See [the safety model](/concepts/safety-model).
## Where to go next
Setup guides for Claude, ChatGPT, Gemini, Mistral Vibe, Perplexity, Cursor, Codex, VS Code and more.
Expose only the tools you use and cut your AI's context cost by up to 88%.
# Google Ads
Source: https://docs.getadloop.com/integrations/google-ads
Reports, keyword research and previewed changes for Search campaigns.
AdLoop reads your Google Ads account (campaigns, ads, keywords, search terms, negatives, recommendations, Performance Max and audiences), runs Keyword Planner research, and changes things through the same preview → dry run → `confirm_and_apply` gate as every other write. New campaigns and responsive search ads are created **PAUSED**. AdLoop builds and edits **Search** campaigns; other campaign types can be analysed but not built.
Google Ads shares one Google connection with [Google Analytics](/integrations/google-analytics), Tag Manager, Search Console and Merchant Center.
## Connect
The onboarding wizard (later: **Settings → Google → Connect Google account**) asks which Google products your AI may use and requests only those permissions. Keep **Google Ads** ticked; Google's consent dialog then asks for access to your Ads data. Google offers no read-only permission for Ads, so for "analyse but never change" use a [read-only API key](/concepts/safety-model#read-only-keys).
Then pick the account. Accounts you reach directly appear in the list; picking a manager account (MCC) opens a second list with the accounts under it, and AdLoop reaches the chosen account through the manager. **I do not use Google Ads** leaves the Ads tools switched off. Agencies connect Google once under **Settings → Google** and pick an Ads account per client under **Settings → Clients** (see [Clients](/cloud/clients)).
**Disconnect Google account** revokes the grant at Google and deletes the stored token. Because all Google products share one grant, Analytics and the others disconnect with it.
Since September 2026 there is no developer token and no Manager Account requirement: Google Ads API access belongs to the Google Cloud project that owns your OAuth client. Enable the Google Ads API there and apply for an access level as described in [Google Cloud project setup](/self-hosting/google-cloud-project).
`adloop init` signs you in, discovers your Ads accounts and stores the default as `ads.customer_id`. Set `ads.login_customer_id` only when you reach accounts through a Manager Account; `list_accounts` then lists the accounts under it. The other Ads tools take a `customer_id` argument to address a different account directly. See the [configuration reference](/self-hosting/configuration).
## Behaviour worth knowing
* **Money is in account currency.** The read tools compute `metrics.cost`, `metrics.cpa` and average CPC for you. Only `run_gaql` returns Google's raw `cost_micros` (divide by 1,000,000).
* **Compact mode for audits.** `get_campaign_performance`, `get_keyword_performance`, `get_search_terms` and `get_ad_performance` accept `compact=true`: account totals, the top ten rows and ready-made offender lists (zero-conversion spenders, low quality scores, wasteful search terms, thin ads) instead of every row.
* **Keyword Planner needs your market.** `discover_keywords` and `estimate_budget` default to Germany and English; pass the `geo_target_id` and `language_id` you actually target.
* **Performance Max: edit, don't build.** `get_pmax_performance` reports asset groups with their Ad Strength and a network breakdown; most PMax traffic lands in Google's catch-all `MIXED` network, so an exact Search vs YouTube vs Display split is not available. Existing asset groups can be edited (assets, signals, product exclusions, status); new PMax campaigns are still built in Google Ads.
* **Recommendations are read, not applied.** `get_recommendations` returns Google's suggestions with estimated impact and flags budget raises that mainly benefit Google; acting on one means drafting the change yourself.
* **Ads clicks above GA4 sessions** are normal in the EU. See [Consent and data gaps](/concepts/consent-and-data).
## Safety specifics
Everything on [the safety model](/concepts/safety-model) applies. On top of that, for Google Ads:
* **The dry run asks Google.** `confirm_and_apply` with `dry_run=true` sends the exact mutates to Google in validate-only mode: Google checks them and executes nothing. `DRY_RUN_FAILED` carries Google's error and means the real apply would fail the same way. Steps that depend on an object an earlier step would create (keywords for a list that does not exist yet, say) cannot be validated and are counted as skipped.
* **Budgets and bids are capped.** `max_daily_budget` caps every budget AdLoop sets. `max_bid_increase_pct` (default 100) caps raises of an ad group's manual CPC and of an existing Maximize Clicks ceiling; a first-time ceiling has no baseline and is not capped.
* **Broad match needs Smart Bidding.** `draft_campaign` refuses BROAD keywords on a campaign without Smart Bidding (Maximize Conversions, Maximize Conversion Value, Target CPA or Target ROAS); `draft_keywords` and `draft_ad_group` flag them in the preview.
* **Targeting is mandatory.** New campaigns need geo and language targets.
* **Landing pages must load.** Final URLs of ads and sitelinks are fetched at draft time; an error page blocks the draft. If the site is only rate-limiting or briefly down, the preview warns and lets you proceed.
* **What is created paused.** Campaigns and responsive search ads are created PAUSED. New ad groups are created enabled but cannot serve until one of their ads is enabled.
* **Rewriting an ad is not free.** Changing headlines or descriptions with `update_responsive_search_ad` keeps the ad's ID, but Google treats the creative as new: its asset-combination learning and performance history reset and it goes back through policy review. The preview says so. URL and display-path edits do not have this effect.
* **Location lists replace, exclusions survive.** `update_campaign` replaces the positive geo and language targets with the list you pass. Negative location exclusions are kept and listed in the preview; add them with `add_negative_locations`.
* **Removal is final.** `remove_entity` cannot be undone; prefer `pause_entity`.
## Tools
Reads: `list_accounts`, `get_campaign_performance` (with search impression share and the share lost to budget or rank), `get_ad_performance` (full RSA copy and policy status, with disapproved and limited ads flagged), `get_change_history` (who changed what in the last 30 days, in the interface, through the API or by auto-applied recommendations), `get_keyword_performance` (quality scores), `get_search_terms`, `get_negative_keywords`, `get_negative_keyword_lists`, `get_negative_keyword_list_keywords`, `get_negative_keyword_list_campaigns`, `get_recommendations`, `get_pmax_performance`, `get_pmax_assets` and `get_detailed_asset_performance` (PMax assets and top asset combinations), `get_audience_performance`, `get_demographic_targeting`, `run_gaql` (any Google Ads Query Language query). Planning: `discover_keywords` and `estimate_budget` (Keyword Planner, read-only).
Writes (all previewed):
* **Structure:** `draft_campaign`, `draft_shopping_campaign` (Standard Shopping for a linked Merchant Center feed), `draft_ad_group`, `update_campaign` (bidding, budget, locations, languages, networks, Maximize Clicks CPC ceiling), `update_ad_group` (name, manual CPC bid).
* **Ads:** `draft_responsive_search_ad`, `update_responsive_search_ad`.
* **Keywords and exclusions:** `draft_keywords`, `add_negative_keywords`, `add_negative_locations`, `draft_demographic_targeting` (age, gender, parental status, income; exclusions by default).
* **Shared negative keyword lists:** `propose_negative_keyword_list` (create and attach), `add_to_negative_keyword_list`, `attach_shared_set_to_campaigns`, `detach_shared_set_from_campaigns`.
* **Campaign assets:** `draft_sitelinks`, `draft_callouts`, `draft_structured_snippets`, `draft_image_assets` (from public image URLs everywhere, uploaded images, or local files when self-hosted; the image is fetched only from public addresses, and if it changes between preview and apply, the apply is refused). For an image without a public URL, see [Uploading images](#uploading-images).
* **Conversion actions:** `draft_create_conversion_action`, `draft_update_conversion_action`, `draft_remove_conversion_action`.
* **Performance Max:** `draft_asset_group_assets` (add and remove text, image and YouTube assets in one change), `get_asset_group_signals` and `draft_asset_group_signals` (search themes, audience signal), `draft_pmax_product_exclusions` (exclude products by item id, brand or custom label).
* **Status:** `pause_entity`, `enable_entity`, `remove_entity` (campaigns, ad groups, ads, keywords, PMax asset groups).
The full list with parameters is in the [tool reference](/reference/tools#google-ads-ads); the toolset slug is `ads`. The tools that join Ads with Analytics (`analyze_campaign_conversions`, `attribution_check`, `landing_page_analysis`) are in the `tracking` toolset, see [Tracking audit](/integrations/tracking-audit).
## Building a campaign
1. **Research.** `discover_keywords` from seed keywords or a URL, then `estimate_budget` for the chosen keywords to see expected clicks and cost.
2. **`draft_campaign`** creates the budget, the campaign (PAUSED), its first ad group, keywords, and geo and language targeting in one plan. More ad groups follow with `draft_ad_group`.
3. **`draft_responsive_search_ad`** per ad group: 3 to 15 headlines of up to 30 characters, 2 to 4 descriptions of up to 90, display paths of up to 15. Headlines and descriptions can be pinned to a position, at some cost to Ad Strength. Several can share a position and rotate there (pin your keyword headlines to position 1, for example), as long as every position can still be filled by a pinned or a separate unpinned asset.
4. **Assets:** sitelinks, callouts and structured snippets on the campaign.
5. **Shared negatives:** a new campaign does not inherit your shared negative keyword lists; attach them with `attach_shared_set_to_campaigns`.
6. **Enable** the ad, then the campaign, with `enable_entity`.
## Shopping and Performance Max
**Standard Shopping.** `draft_shopping_campaign` builds a campaign that advertises a Merchant Center feed: budget, campaign (paused), a product ad group with its Shopping ad and one "All products" group, in one request that the dry run has Google validate. The Merchant Center account has to be linked to the Ads account; the draft checks it. Shopping takes no keywords and no language targeting (the feed's language applies). Bidding is manual CPC, Maximize clicks or target ROAS, and `product_bid` is always needed because Google sets it on the product group. Check `get_merchant_feed_health` before enabling: disapproved products do not serve.
**Editing Performance Max.**
* **Assets:** `draft_asset_group_assets` adds new headlines, long headlines, descriptions, images and YouTube videos and removes old ones in the same change. Replacing an asset that way never drops the group below Google's minimums (three headlines, one long headline, two descriptions with one of at most 60 characters). Images can be public URLs or [uploads](#uploading-images); they are sorted into landscape, square or portrait by their aspect ratio.
* **Signals:** `draft_asset_group_signals` adds search themes or sets the group's audience signal. Signals steer Performance Max; they do not restrict where it serves.
* **Product exclusions:** `draft_pmax_product_exclusions` takes the complete list of item ids, brands or custom label values to exclude from a retail asset group. Asset groups with nested product filters are left to the Google Ads interface.
* **Status:** `pause_entity` and `enable_entity` take `asset_group`.
**Product performance.** `get_shopping_product_performance` sums spend, clicks, conversions, CPA and ROAS per product across Shopping and Performance Max campaigns, and lists the products that spent without converting. Check those for price, stock and landing page before taking them out with `draft_pmax_product_exclusions` or a lower product bid. Which products the feed itself blocks comes from [`get_merchant_product_issues`](/integrations/merchant-center).
## Audiences
`get_audience_lists` shows the account's remarketing and customer lists (size for Search and Display, open or closed), custom segments and combined segments; `search_audience_segments` finds Google's in-market and affinity segments by name. `get_audience_targeting` shows what a campaign or ad group has attached and its audience mode.
`draft_audience_attachment` attaches audiences in one of three ways:
* **Observation** (the default): the audience adjusts bids and shows up in reports, and the campaign keeps its reach.
* **Targeting:** the ads only reach people in the audience. Impressions usually drop sharply, so it needs a double confirmation.
* **Exclusion** (`exclude=True`): keeps people out, e.g. recent buyers on a prospecting campaign.
The draft refuses what Google doesn't allow: custom segments on Search, combined segments outside Search, audiences at both campaign and ad group level, and targeting with a list too small to serve on Search (under 1,000 users). Performance Max takes audiences as asset group signals instead. Remove an audience with `remove_entity` and the `remove_id` from `get_audience_targeting`. Creating audiences stays in the Google Ads interface.
## Not covered yet
Creating audiences, ad schedules and bid adjustments, building new Performance Max campaigns, splitting Shopping product groups by brand or label, Demand Gen and video campaigns, experiments, offline conversion uploads on AdLoop Cloud (the self-hosted edition has them), and applying Google's recommendations. Use the Google Ads interface for these; AdLoop can still report on the campaigns.
## Operations and limits
On AdLoop Cloud, every Google Ads tool call counts toward your workspace's daily operations: reads, Keyword Planner lookups, drafts, dry runs and applies, and the `tracking` tools that read Ads. `health_check` and `list_accounts` are not counted. See [plans and limits](/cloud/plans-and-limits). Self-hosted, the only limits are the ones Google sets for your Cloud project's access level.
## What AdLoop stores
On AdLoop Cloud: an encrypted refresh token, your Google email address, the permissions you granted, and the selected Ads account (ID, name, currency, and the manager account it is reached through). Campaign and performance data pass through and are never stored; the change log records what was changed. Files you upload are the exception: they are kept encrypted for as short as the upload needs (images 24 hours, conversion CSVs until a draft has read them, two hours at most). Self-hosted, the token stays in `~/.adloop/token.json` and the audit log in `~/.adloop/audit.log` on your machine. See the [privacy policy](https://getadloop.com/privacy) for the full description.
## Uploading images
An image on your computer, or one you attached in the chat, has no public URL, and an AI app cannot pass the file itself to a tool. Ask for an upload instead (the AI calls `upload_images`):
* **Apps that show AdLoop cards** (Claude, ChatGPT, VS Code and others with MCP Apps): an upload card appears in the chat. Drop PNG, JPEG or GIF files on it, up to 5 MB each. The card posts the upload ids into the chat when it is done.
* **Other apps on AdLoop Cloud:** the AI gives you an upload link, valid for 30 minutes. Open it in the browser and drop the images, then tell the AI you are done. A coding agent that has the files on disk can upload them itself with `curl -T`.
* **Self-hosted:** the card works in apps that show it; otherwise pass local files directly with `image_paths`.
The AI then creates the assets from the upload ids (`draft_image_assets` with `upload_ids`), with the usual preview and approval. On AdLoop Cloud, uploads also work for Reddit image and carousel ads. Uploads are deleted 24 hours after the upload.
## Offline conversion uploads
Calls and leads that turned into customers offline can go back to Google Ads from a CSV in Google's upload template: `draft_upload_call_conversions` matches calls by the caller's phone number, `draft_upload_enhanced_conversions_for_leads` matches leads by hashed email, phone or name and address. The conversion action named in the file must exist (type UPLOAD\_CALLS or UPLOAD\_CLICKS); the draft checks it, previews the rows with redacted samples, consent and skipped rows, and uploads only after approval.
* **Self-hosted:** pass the file as `csv_path`, or upload it through the card (`upload_conversion_csv`) and pass its `upload_id`.
* **AdLoop Cloud:** not available yet; it arrives with the next version of the data processing agreement, because the files hold your customers' personal data. It will work through the upload card or an upload link.
The file is deleted as soon as a draft has read it, after two hours at the latest. Lead names, emails and phone numbers are hashed (SHA-256) while drafting; callers' numbers go to Google as they are, because Google matches calls by number. The change log never contains numbers, emails or names.
# Google Analytics
Source: https://docs.getadloop.com/integrations/google-analytics
GA4 reports, realtime checks and key events, next to your ad data.
AdLoop reads your GA4 property: custom reports with any dimensions and metrics, realtime data, and the events the property receives. It can also mark an event as a key event, through the same preview → `confirm_and_apply` gate as every other write. Combined with [Google Ads](/integrations/google-ads), it answers the questions neither tool answers alone: real cost per conversion, landing pages that get paid traffic but do not convert, and whether conversions are tracked at all.
Analytics shares one Google connection with Google Ads, Tag Manager, Search Console and Merchant Center.
## Connect
The onboarding wizard (later: **Settings → Google → Connect Google account**) asks which Google products your AI may use. Keep **Google Analytics (GA4)** ticked; Google's consent dialog then asks to view your Analytics data and to manage your Analytics settings. The edit permission is only used by `draft_key_event`; Google has no narrower permission for it.
Then pick the property. **I do not use Google Analytics** switches the Analytics tools off; a workspace needs at least one of Google Ads or Analytics. Agencies pick a property per client under **Settings → Clients** (see [Clients](/cloud/clients)).
If you untick the edit permission in Google's dialog, reports keep working and only key events are unavailable; the dashboard shows the missing permission, and reconnecting restores it with all settings kept.
Enable the **Google Analytics Data API** and the **Google Analytics Admin API** in your Cloud project (see [Google Cloud project setup](/self-hosting/google-cloud-project)). `adloop init` signs you in, finds your properties and stores the default as `ga4.property_id`; every Analytics tool also takes a `property_id` argument for another property.
The sign-in asks for Analytics read and edit access in one go. A token from before AdLoop needed a permission triggers a fresh consent automatically, see [Troubleshooting](/troubleshooting).
## Behaviour worth knowing
* **GA4's own names.** `run_ga4_report` takes dimensions and metrics by their GA4 API names (`sessionSource`, `pagePath`, `deviceCategory`, `sessions`, `totalUsers`, `eventCount`, …). Dates are `today`, `yesterday`, `7daysAgo`, `28daysAgo` or `YYYY-MM-DD`; without a range the report covers the last seven days.
* **Row limits are visible.** A report returns 100 rows unless you raise `limit`, and `total_row_count` tells you how many exist.
* **Realtime for deploys.** `run_realtime_report` shows active users and events right now: the quick way to confirm a new event fires after you ship tracking code.
* **The event inventory.** `get_tracking_events` lists every event name the property received with its count, over the last 28 days by default.
* **Fewer sessions than ad clicks is normal.** GA4 only sees visitors who accepted analytics cookies, while Google Ads counts every click. In EU traffic a ratio of two to five clicks per session is common and not a tracking fault. The `tracking` tools (`analyze_campaign_conversions`, `attribution_check`, `landing_page_analysis`) take this into account. Background in [Consent and data gaps](/concepts/consent-and-data).
## Key events
When an event fires but is not counted as a conversion, `draft_key_event` marks it as a key event. The counting method is `ONCE_PER_EVENT` (purchases) or `ONCE_PER_SESSION` (sign-ups). Two things the preview also tells you:
* It applies to future data only; past events are not reclassified.
* For Google Ads to optimise on it, the property must be linked to the Ads account and the key event imported there as a conversion.
Your Google user needs edit access to the property. The dry run of a key-event plan does not contact Google, so the real apply is the first point where Google checks it.
## Tools
Reads: `get_account_summaries` (accounts and properties you can reach), `run_ga4_report` (with filters, ordering, paging and a comparison period), `run_realtime_report`, `get_tracking_events`, `list_key_events` (which events count as key events), `list_ga4_dimensions_and_metrics` (every field the property offers, custom ones included).
Writes (previewed): `draft_key_event` (mark an event as a key event), `draft_delete_key_event` (stop counting it; the event keeps firing).
The full list with parameters is in the [tool reference](/reference/tools#google-analytics-ga4); the toolset slug is `ga4`. The tools that join Analytics with Google Ads or your codebase (`analyze_campaign_conversions`, `attribution_check`, `landing_page_analysis`, `validate_tracking`, `generate_tracking_code`) are in the `tracking` toolset, and the three-way audit across your code, Tag Manager and GA4 (`audit_event_coverage`) is in `gtm`. How they fit together is on [Tracking audit](/integrations/tracking-audit).
## Operations and limits
On AdLoop Cloud, Analytics tools, `draft_key_event` included, are unmetered and do not count toward your daily operations. The `tracking` tools that also read Google Ads do count. See [plans and limits](/cloud/plans-and-limits). Google's own GA4 API quotas apply on every setup.
## What AdLoop stores
On AdLoop Cloud: the encrypted refresh token shared by all Google products, your Google email address, the permissions you granted, and the selected property (ID and name). Report data passes through and is never stored. Self-hosted, the token stays in `~/.adloop/token.json` on your machine. See the [privacy policy](https://getadloop.com/privacy) for the full description.
# Merchant Center
Source: https://docs.getadloop.com/integrations/merchant-center
Check feed health before blaming Shopping or Performance Max campaigns.
Disapproved products don't serve, and a Shopping or Performance Max campaign starved by its feed looks like a bidding or budget problem from the Ads side. AdLoop reads Google Merchant Center's product status so your AI can rule the feed in or out first: how many products are approved, pending or disapproved, which issues affect the most products, and which account-level issues block serving altogether.
Google's Merchant API has **no read-only permission**: the only one available (`content`) also allows managing products. AdLoop only reads with it and has no tool that changes products or settings.
## Connect
When you connect Google, leave **Merchant Center** ticked under **Access for your AI** ("Read product feed status, for Shopping and PMax"). Then pick the account, during onboarding or later under **Settings → Google → Change**. Agencies pick an account per client under **Settings → Clients**.
Google requires a one-time registration before the Merchant API works with an account, and until then it doesn't even list your accounts. If that is the case, the dashboard asks for your Merchant Center ID instead (shown top right in Merchant Center). When you save the account, AdLoop registers itself with it, so Google's few minutes of processing happen before your first question.
If the page says **Permission missing — reconnect Google to enable Merchant Center**, click **Reconnect** and approve the permission; your other selections stay as they are.
1. Enable the **Merchant API** in your [Google Cloud project](/self-hosting/google-cloud-project).
2. Call `get_merchant_feed_health` with your Merchant Center ID. On first use AdLoop registers your Google Cloud project with that Merchant Center account, a one-time step the Merchant API requires. Google then needs about five minutes before data flows; the tool says so, and a retry afterwards works.
Self-hosted AdLoop asks for all Google permissions in one consent, Merchant Center included. Choosing permissions individually is an AdLoop Cloud feature.
The registration needs a Google user with **Admin** access on the Merchant Center account (Merchant Center → Settings → People and access). If it fails, the tool returns that hint and a link to [Google's manual registration steps](https://developers.google.com/merchant/api/guides/quickstart/registration).
## Behaviour worth knowing
* **Which account is checked.** On AdLoop Cloud, the Merchant Center tools use the Merchant Center account selected for the workspace or the client, so your AI doesn't need the ID. Self-hosted, set `merchant.account_id` in the [configuration](/self-hosting/configuration) to get the same default. An `account_id` passed in the call always wins; your AI finds the IDs with `list_merchant_accounts`.
* **What comes back:** approved, pending and disapproved counts per reporting context (Shopping ads, free listings, …) and country; the top product issues ranked by affected products, each with Google's documentation link; account-level issues with their severity. `CRITICAL` means offers stop serving entirely.
* **Product by product:** `get_merchant_product_issues` names the products behind those counts: disapproved (the default), limited or pending ones, each with its offer id, title and click potential, and every issue with the attribute it concerns, the countries affected and whether you have to act or Google is still reviewing. Products with high click potential come first, because fixing them brings back the most traffic. Filter by an issue code to get, say, every product that lacks a GTIN.
* **Product status lags about 30 minutes.** A feed fix you just made won't show yet.
## Tools
* `list_merchant_accounts`: the Merchant Center accounts your Google account can access
* `get_merchant_feed_health`: product status totals, top issues and account issues for one account
* `get_merchant_product_issues`: the individual products with issues, and their issues
For spend and conversions per product, see `get_shopping_product_performance` under [Google Ads](/integrations/google-ads#shopping-and-performance-max).
Parameters are in the [tool reference](/reference/tools#merchant-center-merchant); the toolset slug is `merchant` ([toolsets](/concepts/toolsets)).
## Limits
* Read-only: AdLoop can't edit products, the feed or account settings. Product issues list up to 1,000 matching products per call; narrow by issue code for larger feeds.
* On AdLoop Cloud, Merchant Center calls are unmetered on every plan; see [plans and limits](/cloud/plans-and-limits).
* On a solo plan or a client-scoped key, calls for an account other than the selected one are refused.
## What AdLoop stores
On AdLoop Cloud: the selected Merchant Center ID and account name, next to your encrypted Google token. Feed data passes through and is never stored.
# PageSpeed
Source: https://docs.getadloop.com/integrations/pagespeed
Lighthouse and real-user Core Web Vitals for your landing pages.
A slow landing page lowers Quality Score, raises click costs and loses visitors before the page has rendered. `analyze_page_speed` runs Google PageSpeed Insights on a URL and returns two views of it: a **Lighthouse** lab test, and **field data** from real Chrome users (the Chrome UX Report) where Google has enough of it.
No Google account is involved: PageSpeed Insights fetches the page itself, so there is nothing to connect.
## Set up
Nothing to set up. Include the `web` toolset in your API key, or leave the key unrestricted ([toolsets](/concepts/toolsets)).
It works without configuration, but keyless requests share a very low quota and can fail with `429`. For a stable quota, enable the **PageSpeed Insights API** in your [Google Cloud project](/self-hosting/google-cloud-project), create an API key under **APIs & Services → Credentials**, and store it as `pagespeed.api_key` (the `adloop init` wizard offers this step). See the [configuration reference](/self-hosting/configuration).
## What you get
* **Performance score** from 0 to 100.
* **Lab metrics:** Largest Contentful Paint, Cumulative Layout Shift, Total Blocking Time, First Contentful Paint.
* **Field metrics** over the last 28 days: the 75th percentile for LCP, Interaction to Next Paint and CLS, each rated fast, average or slow, plus an overall rating.
* **Top opportunities:** the largest Lighthouse improvements with their estimated savings.
* **Insights** that flag a score below 50, a slow real-user rating, or an LCP above the 2.5 second threshold.
## Behaviour worth knowing
* **Mobile by default**, because that's where most paid traffic lands; pass `strategy="desktop"` for the other view.
* **A run takes 10 to 30 seconds.** That's Lighthouse loading the page, not a hang.
* **Field data beats lab data.** Lab numbers come from a simulated load. When the two disagree, trust the real-user numbers. No field data means the page has too little Chrome traffic, not that it's fast. In that case AdLoop falls back to the field data for the whole site (origin) and says so, which is a useful signal for low-traffic landing pages.
* **Public pages only.** Google fetches the URL from its own servers, so localhost, staging behind a login or pages blocked by a firewall can't be measured.
* It pairs with [`landing_page_analysis`](/integrations/tracking-audit#tools): a page with paid traffic, no conversions and a poor score needs speed work before new ad copy.
## Tools
* `analyze_page_speed`: PageSpeed Insights for one URL
Parameters are in the [tool reference](/reference/tools#web-performance-web); the toolset slug is `web`.
## Limits and storage
On AdLoop Cloud, PageSpeed calls are unmetered on every plan; see [plans and limits](/cloud/plans-and-limits). AdLoop stores nothing for this tool: the URL and the results pass through.
# Reddit Ads
Source: https://docs.getadloop.com/integrations/reddit-ads
A second ad platform with the same tools and the same safety model.
AdLoop connects Reddit Ads next to Google Ads: performance reports, campaign structure, pixel health, targeting lookups, and changes that go through the same preview → dry run → `confirm_and_apply` gate as every Google change. New Reddit campaigns, ad groups and ads are created **PAUSED**.
Reddit is a separate connection: its own OAuth grant, its own ad account selection. Use it next to Google or on its own: a workspace with only Reddit Ads connected works too.
## Connect
**Settings → Reddit → Connect Reddit account.** Reddit's own consent dialog asks for `adsread` and `adsedit` (read and edit advertising data); AdLoop never sees your password. Then pick the ad account your AI may reach. Agencies pick a Reddit ad account per client under **Settings → Clients**.
Disconnecting revokes the grant at Reddit and deletes the stored token. Google and your API keys are untouched. You can also withdraw access at any time at [reddit.com/prefs/apps](https://www.reddit.com/prefs/apps).
Reddit has no developer token and no approval process, but you need your own developer application:
1. In Reddit Ads Manager open **Business Manager → Developer Application → Create app** (business admins only).
2. Set the redirect URL to exactly `http://localhost:8765/callback`. Reddit requires an exact match, which is why the wizard uses a fixed port.
3. Run `adloop init` and complete the **Reddit Ads** step with the app ID and secret. It opens Reddit's consent page, stores the refresh token at `~/.adloop/reddit_token.json`, and lets you pick the default ad account.
Reddit throttles requests without a descriptive `User-Agent`; AdLoop builds one from your app ID and Reddit username (`reddit.username` in the config), or you set `reddit.user_agent` yourself.
## What is different from Google Ads
| | Google Ads | Reddit Ads |
| - | - | - |
| Account argument | `customer_id` | `ad_account_id` |
| Structure | campaign → ad group → ad, keywords | campaign → ad group → ad (a post); targeting is communities, interests, keywords, geos, languages |
| Budget | campaign budget | on the **ad group**, unless the campaign uses campaign budget optimization |
| Status | `status` | `configured_status` (what you set) vs `effective_status` (what Reddit computes: `PENDING_APPROVAL`, `REJECTED`, `PENDING_BILLING_INFO`, …) |
| Quality signals | quality score, search terms | pixel health (`get_reddit_pixels`), `rejection_reason` on ads |
| Dry run | validated against the API | read-side preflight: the target is re-read and the safety caps re-checked; nothing is sent |
| Rate limits | shared developer-token quota | per Reddit user (reporting: 60 requests/min) |
| Ad schedule | campaign ad schedule, account time zone | `schedule` on the ad group (or on a CBO campaign), applied in each **viewer's** local time |
Money is returned in the account's currency; Reddit's microcurrency is already divided. Report data lags up to six hours.
### Weekly schedules
Ads Manager's "time of day" grid is the ad group's `schedule`: a list of blocks such as `{"days": "MON-FRI", "start_hour": 13, "end_hour": 23}` (day names, hours 0 to 23, `end_hour` inclusive) or the native `{"start_day": "FRI", "start_hour": 22, "end_day": "SAT", "end_hour": 3}` for overnight windows. `get_reddit_ad_groups` returns it with a readable `schedule_summary` (`Mon–Fri 13:00–23:59`, or `any time`); `update_reddit_ad_group` and `draft_reddit_ad_group` accept it, and `[]` clears it. Two things worth knowing: Reddit applies the hours in each viewer's local time, so a European account keeps delivering to US viewers until the next morning, and zero impressions outside the schedule, a whole weekend included, is by design. The tools say so in `insights[]` before you go looking for a fault.
## Tools
Reads: `list_reddit_accounts`, `list_reddit_funding_instruments`, `get_reddit_campaigns`, `get_reddit_ad_groups`, `get_reddit_ads`, `get_reddit_performance` (with `compact` mode and `insights[]`), `run_reddit_report` (raw fields and breakdowns), `get_reddit_pixels`, `get_reddit_audiences` (custom and saved audiences, and which ad groups use them), `search_reddit_targeting` (including Reddit's related-community suggestions), `get_reddit_account_history` (who changed what), `estimate_reddit_ad_group` (audience, delivery and bid estimates before you build).
Writes (all previewed): `pause_reddit_entity`, `enable_reddit_entity`, `remove_reddit_entity` (archive, irreversible, double confirmation), `update_reddit_campaign`, `update_reddit_ad_group`, `update_reddit_ad` (landing URL, name, comments), `draft_reddit_campaign`, `draft_reddit_ad_group`, `draft_reddit_ad`. Keywords and geolocations in an ad-group draft are validated with Reddit before the preview. Ad groups can also target devices (desktop or mobile, by operating system and version), mobile carriers and view modes; `search_reddit_targeting` looks up valid devices and carriers.
### Creative
`get_reddit_ads` with `include_copy=true` returns each ad's post: type (TEXT, IMAGE, VIDEO, CAROUSEL), headline, body, destination URL, call to action and media, one request per distinct post, so a creative review does not need a browser. `draft_reddit_ad` either creates a post and the ad, or with `post_id` promotes a post that already exists, keeping its upvotes and comments. New posts can be text, image, video (an MP4 or MOV link plus a thumbnail) or a carousel of 2 to 6 image cards, each with its own caption and link. Reddit downloads the media from the URLs you give, so they have to be direct links to the files; the draft checks each one and refuses a web page or a file over Reddit's size limit. On AdLoop Cloud, an image without a public URL can be uploaded first ([Uploading images](/integrations/google-ads#uploading-images)) and used as `upload:`. Two Reddit rules the tools enforce before the dry run: a live post's headline and body cannot be edited (draft a new post for new copy), and a TEXT ("free form") ad carries no `click_url` at all, because it opens the post itself and the link has to be in the body. Placements are the `locations` list on the ad group: `FEED` and/or `COMMENTS_PAGE`.
The full list with parameters is in the [tool reference](/reference/tools#reddit-ads-reddit); the toolset slug is `reddit`.
## Building a campaign
The order is campaign → ad group → ad, each created paused and each through its own preview:
1. **Pixel first.** Every ad group needs a `conversion_pixel_id` (a Reddit rule since July 2026). `get_reddit_pixels` lists them and shows when each event last fired; a CONVERSIONS campaign on a pixel that never fired the optimization event cannot learn.
2. `list_reddit_funding_instruments` for a servable billing instrument and the profile that will author the post.
3. `search_reddit_targeting` for community, interest, geolocation and language ids (`community_suggestions` finds related subreddits). Untargeted ad groups are refused.
3a. `estimate_reddit_ad_group` with the same targeting and budget: audience size, expected delivery, and Reddit's suggested bid range.
4. `draft_reddit_campaign` → `draft_reddit_ad_group` (budget, bid, pixel, targeting) → `draft_reddit_ad` (headline, verified click URL, call to action). IMAGE, VIDEO and CAROUSEL ads click through to the landing page; a TEXT ad clicks through to the post itself, so the link has to be in the body (the tool insists on it). Posts cannot be edited afterwards.
5. Enable the ad, then the ad group, then the campaign. Enabling an ad sends it to Reddit's policy review; expect `PENDING_APPROVAL` for up to a day, then check `get_reddit_ads` for a `rejection_reason`.
## Retargeting audiences
`draft_reddit_custom_audience` builds an audience from data Reddit already has, so nothing personal is uploaded:
* **Website visitors** (`PIXEL_RETARGETING`): people the pixel saw on your site in the last 1 to 90 days, for the events you pick (every page visit, or warmer signals such as add to cart, sign up or a custom event).
* **Engaged people** (`ENGAGEMENT_RETARGETING`): people who saw, clicked, upvoted, commented on or watched the ads of campaigns you name, in the last 1 to 180 days.
Reddit fills a new audience over a few hours and needs roughly 1,000 matched people before an ad group delivers to it. `get_reddit_audiences` shows each audience's status, estimated size and the ad groups that use it. To use one, set `custom_audience_ids` on an ad group (it narrows the ad group to that audience) or `excluded_custom_audience_ids` (it keeps those people out, for example buyers in a prospecting ad group).
Customer lists (emails or phone numbers) are not supported: they would upload personal data, which needs its own consent and legal review.
## Operations and limits
On AdLoop Cloud, Reddit calls count toward the same daily operations ceiling as Google Ads calls (both platforms together); Analytics, Tag Manager, Search Console and Merchant Center stay unmetered. The dashboard shows the split. Reddit never touches the shared Google quota.
## What AdLoop stores
An encrypted refresh token, your Reddit username, the granted scopes, and the selected ad account (id, name, currency, time zone). Campaign and performance data pass through and are never stored. See the [privacy policy](https://getadloop.com/privacy) for the full description.
# Search Console
Source: https://docs.getadloop.com/integrations/search-console
Organic search data next to your paid campaigns.
AdLoop reads Google Search Console's search analytics: clicks, impressions, click-through rate and average position, broken down by query, page, country, device or date. Next to Google Ads data that answers questions like "am I paying for clicks I'd get organically anyway?", "which organic queries should I bid on?" and "which pages lost traffic, and when?".
Access is **read-only** (`webmasters.readonly`).
## Connect
When you connect Google, leave **Search Console** ticked under **Access for your AI** ("Read search data"). Then pick a property, during onboarding or later under **Settings → Google → Change**. Both kinds are listed: a **domain property** (`sc-domain:example.com`, covers every subdomain and protocol) and a **URL property** (`https://example.com/`, only that prefix).
Choosing a property is optional. Without one, your AI passes the property on each call (it finds them with `list_gsc_sites`). Agencies pick a property per client under **Settings → Clients**.
If the page says **Permission missing — reconnect Google to enable Search Console**, click **Reconnect** and approve the permission; your other selections stay as they are.
1. Enable the **Search Console API** in your [Google Cloud project](/self-hosting/google-cloud-project).
2. Run `adloop init`. The optional services step discovers your properties and lets you pin one as `gsc.site_url` in the [config](/self-hosting/configuration), written exactly as Search Console shows it (`https://example.com/` or `sc-domain:example.com`). Leave it empty and the tools take `site_url` per call.
Your Google account needs access to the property in Search Console.
## Behaviour worth knowing
* **Data lags about two days.** Don't compare a Search Console window with an Ads or GA4 window that ends today; end both a few days back.
* **Search types.** `web` by default; also `image`, `video`, `news`, `discover` and `googleNews`.
* **Filters.** Narrow a report by query, page, country or device, for example only queries containing your brand name.
* **Rows.** 100 by default, up to 25,000 per report. There is no paging beyond that, so filter or shorten the date range for very large sites.
* **Dates** take ISO dates or relative values such as `28daysAgo` and `today`.
## Tools
* `list_gsc_sites`: the properties your Google account can access, with your permission level
* `run_gsc_report`: a search analytics report for one property. By default it returns final data, about two days behind; `data_state: "all"` includes the fresh, still-changing days. Large reports page with `start_row` past Google's limit of 25,000 rows per request.
Parameters are in the [tool reference](/reference/tools#search-console-gsc); the toolset slug is `gsc` ([toolsets](/concepts/toolsets)).
## Limits
* Only search analytics is covered. Sitemaps, URL inspection and indexing reports are not part of AdLoop.
* On AdLoop Cloud, Search Console calls are unmetered on every plan; see [plans and limits](/cloud/plans-and-limits).
* On a solo plan or a client-scoped key, calls for a property other than the selected one are refused.
## What AdLoop stores
On AdLoop Cloud: the selected property URL, next to your encrypted Google token. Search data passes through and is never stored. Self-hosted: only `gsc.site_url` in `config.yaml`.
# Tag Manager
Source: https://docs.getadloop.com/integrations/tag-manager
Read your GTM container and find out why an event never reaches GA4.
AdLoop reads Google Tag Manager: accounts, containers, tags, triggers, variables, workspaces and the version history. The point is diagnosis. When a conversion goes missing, your AI can see which tag should send it, whether that tag is paused, what its trigger matches, and whether someone edited it without publishing.
On AdLoop Cloud, access is **read-only** (`tagmanager.readonly`): AdLoop can't change, create or publish anything in a container. The self-hosted edition can also make changes, as an opt-in; see [Changes](#changes-self-hosted-opt-in).
## Connect
When you connect Google, leave **Tag Manager** ticked under **Access for your AI** ("Read containers, for tracking audits"). Then pick the Tag Manager account and the container, either during onboarding or later under **Settings → Google → Change**. With a single GTM account, the account step is skipped.
Choosing a container is optional. Without one, the dashboard shows "Not selected — GTM tools ask per call", and your AI passes the IDs on each call (it finds them with `list_gtm_accounts` and `list_gtm_containers`). Agencies pick a container per client under **Settings → Clients**.
If the page says **Permission missing — reconnect Google to enable Tag Manager**, your Google connection was made without this permission (deselected, or from an earlier version). Click **Reconnect** and approve it; your other selections stay as they are.
1. Enable the **Tag Manager API** in your [Google Cloud project](/self-hosting/google-cloud-project).
2. Run `adloop init`. The optional services step discovers your containers and lets you pin one as `gtm.account_id` / `gtm.container_id` in the [config](/self-hosting/configuration). Leave it empty and the tools take `gtm_account_id` / `gtm_container_id` per call.
Your Google account needs at least **Read** access on the container. A token created before AdLoop added Tag Manager is refreshed with a new consent automatically; see [Adding scopes later](/self-hosting/google-cloud-project#adding-scopes-later).
## What it reads
* **The live container.** Tags, triggers and the event-coverage audit look at the **published** version, which is what actually runs on your site. Trigger conditions are turned into readable text, such as `{{Page Path}} matches RegExp ^/pricing/`.
* **Unpublished drafts.** `get_gtm_workspace_diff` lists what was added, changed or deleted in a workspace but never published, plus merge conflicts. This is the usual cause of "I fixed the tag and nothing happened".
* **History.** `list_gtm_versions` and `get_gtm_version` list the published versions in order, with their names, notes and contents, so a drop in conversions can be matched to a specific publish. Google's API doesn't say who published a version or when; the version history in GTM itself shows that.
* **Variables.** Custom variables plus the enabled built-ins. A trigger that uses `{{Form ID}}` never fires if that built-in isn't enabled.
## The event coverage audit
`audit_event_coverage` compares three sources: the events your website code sends, the GA4 event tags in the live container, and the events GA4 actually receives. Each event gets a status, for example `ok`, `tag_paused`, `tag_active_but_not_firing`, `gtm_paused_but_firing` or `ga4_only`. It is the GTM half of the [tracking audit](/integrations/tracking-audit), which explains the workflow. The audit also needs a GA4 property.
## Tools
* `list_gtm_accounts`, `list_gtm_containers`: find account and container IDs
* `list_gtm_tags`, `get_gtm_tag`: tag inventory, or the full configuration of one tag. With `workspace_id`, the tag and trigger tools read a workspace's unpublished state instead of the live container
* `list_gtm_triggers`, `get_gtm_trigger`: trigger inventory with readable conditions, or one trigger with every tag that uses it
* `list_gtm_variables`: custom and enabled built-in variables
* `list_gtm_workspaces`, `get_gtm_workspace_diff`: drafts and their unpublished changes
* `list_gtm_versions`, `get_gtm_version`: publish history and the contents of one version
* `get_gtm_version_diff`: what changed between two versions (by default, the latest version against the one before it): tags, triggers and variables added, removed or changed
* `audit_event_coverage`: code ↔ GTM ↔ GA4 comparison per event
Parameters are in the [tool reference](/reference/tools#tag-manager-gtm); the toolset slug is `gtm` ([toolsets](/concepts/toolsets)).
## Changes (self-hosted, opt-in)
The self-hosted edition can create and update tags, triggers and variables, delete them, publish a workspace, and roll the container back to an earlier version: `draft_gtm_tag`, `draft_gtm_trigger`, `draft_gtm_variable`, `draft_delete_gtm_entity`, `draft_publish_gtm_workspace` and `draft_rollback_gtm_version`. It's off until you set `gtm.write_enabled: true` in the [config](/self-hosting/configuration). Turning it on asks for a new Google consent with the Tag Manager edit and publish permissions, and your Google account needs Edit (and Publish) rights on the container.
* Tag and trigger drafts only change a **workspace**. Nothing reaches your site until a publish is applied, and the publish preview lists every pending change in the workspace, including other people's edits.
* **Custom HTML** tags and **Custom JavaScript** variables are refused unless `gtm.allow_custom_html: true` is set as well, also when a publish or rollback would bring one live.
* **Variables:** Data Layer Variables, the usual source for GA4 ecommerce values, need just a name and the dataLayer key (`ecommerce.value`, `ecommerce.transaction_id`, …). Renaming a variable that tags or triggers use as `{{name}}` is refused, because the references would break (rename it in the Tag Manager interface instead), and so is deleting one that is still in use.
* Every change goes through the [preview and confirm flow](/concepts/safety-model). A publish returns the version it replaced, which is your way back.
## Limits
* Dynamic-event tags (an event name taken from `{{Event}}`) and Custom HTML tags can't be interpreted automatically. The audit lists them separately for a manual look.
* On AdLoop Cloud, Tag Manager calls are unmetered on every plan; see [plans and limits](/cloud/plans-and-limits).
* On a solo plan or a client-scoped key, calls for a container other than the selected one are refused.
## What AdLoop stores
On AdLoop Cloud: the selected GTM account ID, container ID and container name, next to your encrypted Google token. Container contents pass through and are never stored. Self-hosted: only the IDs you put in `config.yaml`.
# Tracking audit
Source: https://docs.getadloop.com/integrations/tracking-audit
Compare what your website code sends with what GTM and GA4 actually record.
Most "my campaign doesn't convert" problems are tracking problems: an event that was never deployed, a GTM tag left paused, a trigger that never matches, or a GA4 event that isn't marked as a key event. AdLoop's tracking audit puts your website code, your Tag Manager container, GA4 and Google Ads side by side so your AI can find the break instead of guessing.
There is nothing extra to connect. The audit uses your Google Analytics connection, plus [Tag Manager](/integrations/tag-manager) for the GTM comparison and Google Ads for the cross-checks.
## How the code part works
AdLoop never reads your files, on AdLoop Cloud or self-hosted. Your AI client (Claude Code, Cursor, …) searches your codebase itself for `gtag('event', …)` and `dataLayer.push({event: …})` calls, collects the event names, and passes that list to the tool. So the code comparison needs an AI client with access to the repository; in a chat without your code, your AI can still run the GA4, GTM and Ads checks.
## The workflow
1. **Rule out consent first.** In the EU, Ads usually shows several times more clicks than GA4 shows sessions because many visitors decline analytics cookies. That's normal, not broken tracking; see [consent and data gaps](/concepts/consent-and-data).
2. **Is anything missing?** `attribution_check` compares the conversions Google Ads reports with what GA4 records, optionally for named events such as `sign_up` or `purchase`, and says whether a gap is explained by consent.
3. **Code against GA4.** `validate_tracking` takes the event names from your code and returns which ones GA4 receives, which are missing, which arrive without a code reference, and which GA4 collects automatically. It looks at the last 28 days by default.
4. **Code against GTM against GA4.** `audit_event_coverage` adds the live Tag Manager container and gives every event a status:
| Status | Meaning |
| - | - |
| `ok` | Tag active, event arriving in GA4 |
| `ok_auto_collected` | GA4 collects it automatically (Enhanced Measurement), no tag needed |
| `tag_paused` | The GTM tag exists but is paused |
| `tag_active_but_not_firing` | Tag active, but GA4 receives nothing: usually a trigger problem |
| `no_tag_no_fire` | In your code, but no tag and no hits |
| `ga4_fires_no_tag` | In your code and arriving in GA4, without a GTM tag |
| `gtm_only_firing` / `gtm_only_not_firing` | A tag not referenced in your code, with or without hits |
| `gtm_paused_but_firing` | Only paused tags, yet GA4 receives the event: it comes from somewhere else (hardcoded gtag, another container, an app) |
| `ga4_only` | Arrives in GA4 with no tag and no code reference |
| `auto_event_only` | An automatic GA4 event your code doesn't reference |
Your AI drills into flagged tags and triggers with the [Tag Manager tools](/integrations/tag-manager#tools).
5. **Fix it.** `generate_tracking_code` writes a ready-to-paste gtag snippet, with Google's recommended parameters for standard events and an optional form-submit, button-click or page-load listener, and checks whether the event already arrives in GA4. If an event arrives but isn't a key event, `draft_key_event` marks it as one, through the usual [preview and confirmation](/concepts/safety-model).
## Paid traffic against GA4
For "what do my ads really achieve?":
* `analyze_campaign_conversions`: Ads clicks and cost per campaign against GA4 sessions and conversions, the real cost per conversion, and the click-to-session ratio as a consent indicator
* `landing_page_analysis`: paid sessions, conversion rate and engagement per ad landing page, including pages that get clicks but never convert. A slow page is worth a [PageSpeed](/integrations/pagespeed) check.
## Tools
* `attribution_check`, `analyze_campaign_conversions`, `landing_page_analysis`: Google Ads ↔ GA4 cross-checks
* `validate_tracking`: your code's events ↔ GA4
* `generate_tracking_code`: gtag snippets
* `audit_event_coverage`: your code's events ↔ live GTM container ↔ GA4
All of them only read. They sit in the `tracking` toolset, except `audit_event_coverage`, which belongs to `gtm` ([toolsets](/concepts/toolsets)). Parameters are in the [tool reference](/reference/tools).
## Limits
* Tag Manager tags with a dynamic event name (`{{Event}}`) and Custom HTML tags can't be interpreted automatically; the audit lists them for a manual look.
* The audit compares the **published** container. Unpublished edits show up in `get_gtm_workspace_diff`.
* On AdLoop Cloud, `analyze_campaign_conversions`, `landing_page_analysis` and `attribution_check` query Google Ads and count as Google Ads operations. The other tracking tools are unmetered. See [plans and limits](/cloud/plans-and-limits).
## What AdLoop stores
Nothing beyond your existing connections. Event names, tag configurations and report data pass through.
# Quickstart: AdLoop Cloud
Source: https://docs.getadloop.com/quickstart/cloud
From registration to your first analysis in under five minutes.
AdLoop Cloud hosts everything for you: Google API access, OAuth, credential encryption, and the MCP endpoint at `mcp.getadloop.com`. You bring a Google account with access to your Ads account or GA4 property, and an AI tool that speaks MCP.
Register at [getadloop.com](https://getadloop.com/register). The free plan covers one Ads account and one GA4 property and needs no credit card. [Pricing](https://getadloop.com/pricing) if you need more, and [plans and limits](/cloud/plans-and-limits) for what each plan includes.
The onboarding wizard asks which platforms you use: Google, Reddit, or both. You then sign in to each one in turn and pick which accounts your AI may see: for Google an Ads account, a GA4 property, and optionally a Tag Manager container, Search Console property, or Merchant Center account; for Reddit an ad account.
You can skip a platform in the wizard and connect it later from the sidebar (**Settings → Google** or **Settings → Reddit**).
AdLoop Cloud is verified by Google, so you see Google's normal consent dialog with no warning screen. You pick there which services to grant, AdLoop never sees your password, and you can revoke access in your Google account at any time.
The wizard creates your personal key automatically. It's shown once and stored encrypted; you can reveal, restrict, or revoke keys later under **Settings → API keys**. Optional restrictions: [read-only](/concepts/safety-model#read-only-keys) and [toolsets](/concepts/toolsets).
Follow the guide for your tool: [claude.ai](/connect/claude-ai), [ChatGPT](/connect/chatgpt), [Claude Code](/connect/claude-code), [Cursor](/connect/cursor), or [any other client](/connect/overview): Codex, VS Code, Gemini, Mistral Vibe, Perplexity, Zed and more. The dashboard's **Connect AI** page walks you through each of these, with your key already filled in.
Try: *"How did my campaigns perform in the last 30 days, and where am I wasting spend?"*
## The order matters
Connecting an ad platform (Google or Reddit) comes before API keys, and API keys before connecting your AI. A key created before any account is selected would only ever return setup errors, so the dashboard enforces the sequence.
# Quickstart: Self-hosted
Source: https://docs.getadloop.com/quickstart/self-hosted
Run the open-source MCP server with your own Google Cloud project.
The open-source edition runs on your machine and uses your own (free) Google Cloud project for OAuth. One-time setup takes about five minutes with the wizard.
## Requirements
* Python 3.11+
* A Google Ads account (a Manager Account is optional; only needed to reach several accounts through one login)
* A Google Cloud project with OAuth credentials and Google Ads API access (the wizard walks you through it, see [Google Cloud project setup](/self-hosting/google-cloud-project)). No developer token since September 2026.
## Run the wizard
```bash theme={null}
pip install adloop
adloop init
```
The wizard walks you through:
1. **Google Cloud setup**: your project, the required APIs, an OAuth client
2. **API access and MCC account ID**: both optional prompts now; access is applied for on the Cloud project, the MCC only matters with a Manager Account
3. **OAuth sign-in**: opens a browser, or prints a URL on headless servers
4. **Account discovery**: finds your GA4 properties and Ads accounts automatically
5. **Optional services**: pin a GTM container and Search Console property (auto-discovered), store a PageSpeed API key. Skip any step with Enter.
6. **Safety defaults**: budget cap and dry-run preference
7. **Toolsets**: optionally expose only part of the tool catalog to [cut context cost](/concepts/toolsets)
8. **Editor snippets**: ready-to-paste MCP configuration for Claude Code and Cursor, including your toolset selection
Configuration lives in `~/.adloop/config.yaml`; see the [configuration reference](/self-hosting/configuration).
## Connect your AI
The wizard prints the exact snippets. In short:
```bash theme={null}
claude mcp add --transport stdio adloop -- python -m adloop
```
For other clients, see [Connect your AI](/connect/claude-code).
Prefer zero setup? [AdLoop Cloud](/quickstart/cloud) runs the same tools without a Google Cloud project or an API access application.
# Tool reference
Source: https://docs.getadloop.com/reference/tools
Every AdLoop tool, grouped by toolset.
Tools are grouped by [toolset](/concepts/toolsets). Type: **Read** tools never change anything; **Write** tools return previews and only act through `confirm_and_apply`; **Destructive** writes additionally require double confirmation.
## Always available (`core`)
Included with every toolset selection.
| Tool | Type | What it does |
| - | - | - |
| `confirm_and_apply` | Destructive | Execute a previously previewed change. |
| `health_check` | Read | Test AdLoop connectivity — checks OAuth token, GA4 API, Google Ads API, |
| `list_uploads` | Read | List the files the user uploaded to AdLoop that have not expired. |
| `upload_images` | Read | Let the user upload images (PNG, JPEG, GIF up to 5 MB) for ad assets. |
## Google Ads (`ads`)
Google Ads reads, writes, and planning.
| Tool | Type | What it does |
| - | - | - |
| `add_negative_keywords` | Write | Draft negative keyword additions — returns a PREVIEW. |
| `add_negative_locations` | Write | Draft negative geo location additions — returns a PREVIEW. |
| `add_to_brand_list` | Write | Append brands to an EXISTING brand list — returns a PREVIEW. |
| `add_to_negative_keyword_list` | Write | Append keywords to an EXISTING shared negative keyword list — returns a PREVIEW. |
| `attach_brand_list_to_campaigns` | Write | Attach an existing brand list to one or more campaigns — returns a PREVIEW. |
| `attach_shared_set_to_campaigns` | Write | Attach an existing shared set to one or more campaigns — returns a PREVIEW. |
| `check_brand_names` | Read | Check a list of brand names against Google's brand knowledge graph. |
| `detach_brand_list_from_campaigns` | Destructive | Detach a brand list from one or more campaigns — returns a PREVIEW. |
| `detach_shared_set_from_campaigns` | Write | Detach a shared set from one or more campaigns — returns a PREVIEW. |
| `discover_keywords` | Read | Discover new keyword ideas using Google Ads Keyword Planner. |
| `draft_ad_group` | Write | Draft a new ad group within an existing campaign — returns a PREVIEW, does NOT create. |
| `draft_ai_max_settings` | Write | Draft AI Max controls for a Search campaign — returns a PREVIEW. |
| `draft_asset_group_assets` | Write | Draft adding and/or removing assets of a Performance Max asset group — returns a PREVIEW. |
| `draft_asset_group_signals` | Write | Draft adding search themes or an audience signal to a Performance Max asset group, or removing signals — PREVIEW. |
| `draft_assign_custom_conversion_goal` | Write | Point a campaign at a custom conversion goal — returns a PREVIEW. |
| `draft_audience_attachment` | Write | Draft attaching audiences to a campaign or ad group as observation, targeting or exclusion: returns a PREVIEW. |
| `draft_callouts` | Write | Draft campaign callout assets — returns a PREVIEW. |
| `draft_campaign` | Write | Draft a full campaign structure — returns a PREVIEW, does NOT create anything. |
| `draft_clear_custom_conversion_goal` | Write | Put a campaign back on the account-level goals — returns a PREVIEW. |
| `draft_create_conversion_action` | Write | Draft a new Google Ads ConversionAction — returns a PREVIEW. |
| `draft_custom_conversion_goal` | Write | Create a custom conversion goal — returns a PREVIEW. |
| `draft_demographic_targeting` | Write | Draft demographic targeting (age/gender/parental status/income) — returns a PREVIEW. |
| `draft_image_assets` | Write | Draft campaign image assets from local files, public image URLs or uploads — returns a PREVIEW. |
| `draft_keywords` | Write | Draft keyword additions — returns a PREVIEW, does NOT add keywords. |
| `draft_pmax_product_exclusions` | Write | Draft which products a retail Performance Max asset group excludes — returns a PREVIEW. |
| `draft_prepare_brand_exclusions` | Write | Draft the safe standard state for brand exclusions — returns a PREVIEW. |
| `draft_remove_conversion_action` | Write | Draft a REMOVAL of a ConversionAction — returns a PREVIEW. |
| `draft_responsive_search_ad` | Write | Draft a Responsive Search Ad — returns a PREVIEW, does NOT create the ad. |
| `draft_shopping_campaign` | Write | Draft a Standard Shopping campaign that advertises a Merchant Center feed — returns a PREVIEW. |
| `draft_sitelinks` | Write | Draft sitelink extensions for a campaign — returns a PREVIEW. |
| `draft_structured_snippets` | Write | Draft campaign structured snippet assets — returns a PREVIEW. |
| `draft_update_conversion_action` | Write | Draft a partial UPDATE of an existing ConversionAction — returns PREVIEW. |
| `draft_update_custom_conversion_goal` | Write | Rename and/or re-scope an existing custom conversion goal — PREVIEW. |
| `draft_upload_call_conversions` | Write | Draft an offline CALL-conversion upload from a CSV — returns a PREVIEW. |
| `draft_upload_enhanced_conversions_for_leads` | Write | Draft an Enhanced Conversions for LEADS upload from a CSV — PREVIEW. |
| `enable_entity` | Write | Draft enabling a paused campaign, ad group, ad, or keyword — returns a PREVIEW. |
| `estimate_budget` | Read | Forecast clicks, cost, and conversions for a set of keywords. |
| `get_ad_performance` | Read | Get ad-level performance data including headlines, descriptions, and metrics. |
| `get_ai_max_settings` | Read | Read the AI Max controls of Search campaigns, per campaign and ad group. |
| `get_asset_group_signals` | Read | Search themes and the audience signal of a Performance Max asset group, plus the account's audiences. |
| `get_audience_lists` | Read | The account's own audiences: remarketing and customer lists, custom segments, combined segments. |
| `get_audience_performance` | Read | Get audience segment performance metrics. |
| `get_audience_targeting` | Read | Audiences attached to one campaign or ad group, its exclusions, and the audience mode. |
| `get_brand_list_brands` | Read | List the brands inside a brand list. |
| `get_brand_list_campaigns` | Read | List which campaigns a brand list is attached to. |
| `get_brand_lists` | Read | List all brand lists (SharedSets of type BRANDS) in the account. |
| `get_campaign_performance` | Read | Get campaign-level performance metrics for a date range. |
| `get_change_history` | Read | List recent account changes: who changed what, when, and through which tool. |
| `get_custom_conversion_goals` | Read | Read the custom conversion goals and the campaign goal configuration. |
| `get_demographic_targeting` | Read | List demographic targeting criteria (age, gender, parental status, income). |
| `get_detailed_asset_performance` | Read | Get top-performing asset combinations for Performance Max campaigns. |
| `get_keyword_performance` | Read | Get keyword metrics including quality scores and competitive data. |
| `get_negative_keyword_list_campaigns` | Read | List which campaigns a shared negative keyword list is attached to. |
| `get_negative_keyword_list_keywords` | Read | List the keywords inside a shared negative keyword list. |
| `get_negative_keyword_lists` | Read | List all shared negative keyword lists (SharedSets) in the account. |
| `get_negative_keywords` | Read | List existing negative keywords for a campaign or all campaigns. |
| `get_pmax_assets` | Read | List the assets of Performance Max campaigns with type, status and content. |
| `get_pmax_performance` | Read | Get Performance Max campaign and asset group performance. |
| `get_recommendations` | Read | Retrieve Google's auto-generated recommendations with estimated impact. |
| `get_search_terms` | Read | Get search terms report — what users actually typed before clicking your ads. |
| `get_shopping_product_performance` | Read | Spend, clicks, conversions, CPA and ROAS per product across Shopping and Performance Max. |
| `list_accounts` | Read | List accessible Google Ads accounts. |
| `pause_entity` | Write | Draft pausing a campaign, ad group, ad, or keyword — returns a PREVIEW. |
| `propose_brand_list` | Write | Draft a brand list and optionally attach it to campaigns — returns a PREVIEW. |
| `propose_negative_keyword_list` | Write | Draft a shared negative keyword list and attach it to a campaign — returns a PREVIEW. |
| `remove_entity` | Destructive | Draft REMOVING an entity — returns a PREVIEW. This is IRREVERSIBLE. |
| `remove_from_brand_list` | Destructive | Remove brands from a brand list — returns a PREVIEW. |
| `run_gaql` | Read | Execute an arbitrary GAQL (Google Ads Query Language) query. |
| `search_audience_segments` | Read | Find Google's in-market and affinity segments by name, with the segment\_id to attach. |
| `suggest_brands` | Read | Resolve a brand name to the brands Google recognizes for it. |
| `update_ad_group` | Write | Draft an ad group update for name and/or manual CPC bid — returns a PREVIEW. |
| `update_campaign` | Write | Draft an update to an existing campaign — returns a PREVIEW, does NOT apply. |
| `update_responsive_search_ad` | Write | Update mutable fields on an existing RSA in place — returns a PREVIEW. |
| `upload_conversion_csv` | Read | Let the user upload a conversion CSV for an offline conversion upload. |
## Google Analytics (`ga4`)
Google Analytics reads and key events.
| Tool | Type | What it does |
| - | - | - |
| `draft_delete_key_event` | Destructive | Draft removing a GA4 key event so the event no longer counts as a conversion, returning a PREVIEW. |
| `draft_key_event` | Write | Draft marking a GA4 event as a key event (conversion) — returns a PREVIEW. |
| `get_account_summaries` | Read | List all GA4 accounts and properties accessible by the authenticated user. |
| `get_tracking_events` | Read | List all GA4 events and their volume for the given date range. |
| `list_ga4_dimensions_and_metrics` | Read | List the dimensions and metrics a GA4 property can report on, including custom ones. |
| `list_key_events` | Read | List the key events (conversions) configured on a GA4 property. |
| `run_ga4_report` | Read | Run a custom GA4 report with optional filters, ordering, paging and a comparison period. |
| `run_realtime_report` | Read | Run a GA4 realtime report showing current active users and events. |
## Tracking & Attribution (`tracking`)
Cross-channel attribution and tracking code.
| Tool | Type | What it does |
| - | - | - |
| `analyze_campaign_conversions` | Read | Campaign clicks → GA4 conversions mapping — the real cost-per-conversion. |
| `attribution_check` | Read | Compare Ads-reported conversions vs GA4 — find tracking discrepancies. |
| `generate_tracking_code` | Read | Generate a GA4 event tracking JavaScript snippet. |
| `landing_page_analysis` | Read | Analyze which landing pages convert and which don't. |
| `validate_tracking` | Read | Compare tracking events found in the codebase against actual GA4 data. |
## Tag Manager (`gtm`)
Google Tag Manager reads and (opt-in) writes.
| Tool | Type | What it does |
| - | - | - |
| `audit_event_coverage` | Read | Three-way audit: codebase events ↔ GTM tags ↔ GA4 actual fires. |
| `draft_delete_gtm_entity` | Destructive | Draft deleting a Tag Manager tag, trigger or variable — returns a PREVIEW. |
| `draft_gtm_tag` | Write | Draft creating or updating a Tag Manager tag — returns a PREVIEW. |
| `draft_gtm_trigger` | Write | Draft creating or updating a Tag Manager trigger — returns a PREVIEW. |
| `draft_gtm_variable` | Write | Draft creating or updating a Tag Manager variable: returns a PREVIEW. |
| `draft_publish_gtm_workspace` | Destructive | Draft publishing a Tag Manager workspace LIVE — returns a PREVIEW. |
| `draft_rollback_gtm_version` | Destructive | Draft republishing an older Tag Manager version LIVE; returns a PREVIEW. |
| `get_gtm_tag` | Read | Get the full RAW configuration for a single GTM tag. |
| `get_gtm_trigger` | Read | Get the full RAW configuration for a single GTM trigger. |
| `get_gtm_version` | Read | Get metadata + entity counts for a single GTM container version. |
| `get_gtm_version_diff` | Read | Compare two GTM container versions: what a publish changed. |
| `get_gtm_workspace_diff` | Read | Show drafted-but-not-published changes in a GTM workspace. |
| `list_gtm_accounts` | Read | List all GTM accounts the AdLoop service account / OAuth user can read. |
| `list_gtm_containers` | Read | List all containers under a GTM account. |
| `list_gtm_tags` | Read | List every tag in the LIVE GTM container, or in one workspace. |
| `list_gtm_triggers` | Read | List every trigger in the LIVE GTM container, or in one workspace. |
| `list_gtm_variables` | Read | List GTM variables — both custom and enabled built-in. |
| `list_gtm_versions` | Read | List GTM container version headers (newest first). |
| `list_gtm_workspaces` | Read | List workspaces (drafts) under a GTM container. |
## Search Console (`gsc`)
Search Console reads.
| Tool | Type | What it does |
| - | - | - |
| `list_gsc_sites` | Read | List all Google Search Console properties the authenticated user can access. |
| `run_gsc_report` | Read | Run a Google Search Console search analytics report. |
## Web Performance (`web`)
PageSpeed / web performance.
| Tool | Type | What it does |
| - | - | - |
| `analyze_page_speed` | Read | Run PageSpeed Insights for a landing page — Lighthouse + real-user data. |
## Merchant Center (`merchant`)
Merchant Center reads.
| Tool | Type | What it does |
| - | - | - |
| `get_merchant_feed_health` | Read | Merchant Center feed health — disapproved products + account issues. |
| `get_merchant_product_issues` | Read | The products behind the feed-health counts, each with its issues. |
| `list_merchant_accounts` | Read | List Google Merchant Center accounts the connected user can access. |
## Reddit Ads (`reddit`)
Reddit Ads reads, writes, and planning.
| Tool | Type | What it does |
| - | - | - |
| `draft_reddit_ad` | Write | Draft a Reddit ad: creates a post on the profile, then the ad (PAUSED) — PREVIEW. |
| `draft_reddit_ad_group` | Write | Draft a new Reddit ad group (created PAUSED) — returns a PREVIEW. |
| `draft_reddit_campaign` | Write | Draft a new Reddit campaign (created PAUSED) — returns a PREVIEW. |
| `draft_reddit_custom_audience` | Write | Draft a Reddit retargeting audience (website visitors or engaged people) — PREVIEW. |
| `enable_reddit_entity` | Write | Draft enabling (configured\_status=ACTIVE) a Reddit campaign, ad group or ad — PREVIEW. |
| `estimate_reddit_ad_group` | Read | Audience size, delivery estimate and Reddit's suggested bid for a planned ad group — read-only. |
| `get_reddit_account_history` | Read | Who changed what in the Reddit ad account: field, before/after, member, time. |
| `get_reddit_ad_groups` | Read | List Reddit ad groups (budget, bid, pixel, weekly schedule, targeting summary), optionally per campaign. |
| `get_reddit_ads` | Read | List Reddit ads with status, rejection\_reason, post and click URL. |
| `get_reddit_audiences` | Read | Reddit custom and saved audiences of the account, and which ad groups use them. |
| `get_reddit_campaigns` | Read | List Reddit campaigns with status, objective, budget mode and bids. |
| `get_reddit_performance` | Read | Reddit Ads performance: spend, impressions, clicks, CTR, CPC, conversions, CPA, ROAS. |
| `get_reddit_pixels` | Read | Reddit pixels of the account and when each event (purchase, sign\_up, lead, ...) last fired. |
| `list_reddit_accounts` | Read | List Reddit businesses and ad accounts the connected Reddit user can access. |
| `list_reddit_funding_instruments` | Read | Funding instruments (billing) and posting profiles of a Reddit ad account. |
| `pause_reddit_entity` | Write | Draft pausing a Reddit campaign, ad group or ad — returns a PREVIEW. |
| `remove_reddit_entity` | Destructive | Draft ARCHIVING a Reddit campaign, ad group or ad — irreversible, PREVIEW. |
| `run_reddit_report` | Read | Run a custom Reddit Ads report for metrics get\_reddit\_performance omits. |
| `search_reddit_targeting` | Read | Look up targeting options for draft\_reddit\_ad\_group. |
| `update_reddit_ad` | Write | Draft changes to a Reddit ad — name, landing URL (click\_url), comments on/off. |
| `update_reddit_ad_group` | Write | Draft changes to a Reddit ad group — budget, bid, run dates, weekly schedule, targeting. |
| `update_reddit_campaign` | Write | Draft changes to a Reddit campaign — name, run dates, spend cap, and (CBO only) budget/bid/schedule. |
# Configuration reference
Source: https://docs.getadloop.com/self-hosting/configuration
Everything in ~/.adloop/config.yaml.
All configuration lives in `~/.adloop/config.yaml`, created by `adloop init`. A documented template ships as `config.yaml.example` in the repository.
| Section | Key | Default | Description |
| - | - | - | - |
| `google` | `project_id` | *(empty)* | Google Cloud project ID (only needed with custom credentials) |
| `google` | `credentials_path` | *(empty)* | Path to OAuth client JSON or service account key. Empty = `~/.adloop/credentials.json`, else Application Default Credentials |
| `google` | `token_path` | `~/.adloop/token.json` | Where the OAuth token is stored (auto-created) |
| `ga4` | `property_id` | — | Your GA4 property ID (auto-discovered by the wizard) |
| `ads` | `developer_token` | — | Legacy, optional: API access belongs to your Cloud project since September 2026 |
| `ads` | `customer_id` | — | Default Ads customer ID (auto-discovered by the wizard) |
| `ads` | `login_customer_id` | — | Your MCC account ID, only with a Manager Account |
| `gsc` | `site_url` | *(empty)* | Search Console property (`https://example.com/` or `sc-domain:example.com`); empty = pass per call |
| `gtm` | `account_id` / `container_id` | *(empty)* | Default GTM account/container; empty = pass per call |
| `merchant` | `account_id` | *(empty)* | Default Merchant Center account for `get_merchant_feed_health`; empty = pass per call |
| `pagespeed` | `api_key` | *(empty)* | Optional API key for `analyze_page_speed` (keyless works at low rates) |
| `reddit` | `client_id` / `client_secret` | *(empty)* | Your Reddit developer app (Business Manager → Developer Application); see [Reddit Ads](/integrations/reddit-ads) |
| `reddit` | `ad_account_id` | *(empty)* | Default Reddit ad account for every Reddit tool (picked by the wizard) |
| `reddit` | `username` | *(empty)* | Your Reddit username, only used in the User-Agent Reddit requires |
| `reddit` | `token_path` | `~/.adloop/reddit_token.json` | Where the Reddit refresh token is stored |
| `safety` | `max_daily_budget` | `50.00` | Maximum daily budget AdLoop can set per campaign |
| `safety` | `max_bid_increase_pct` | `100` | Largest bid raise one change may make, in percent (Google Ads CPC bids and Maximize Clicks ceilings, Reddit bids) |
| `safety` | `require_dry_run` | `true` | Force all writes to dry-run mode |
| `safety` | `two_phase_apply` | `false` | Refuse real applies until the plan had a dry-run pass |
| `safety` | `blocked_operations` | `[]` | Operations to block entirely |
## Environment variables
| Variable | Effect |
| - | - |
| `ADLOOP_CONFIG` | Path to an alternative config file |
| `ADLOOP_TOOLSETS` | Comma-separated [toolsets](/concepts/toolsets) to expose (unset = all) |
## Multiple setups
Point `ADLOOP_CONFIG` at different config files to run separate accounts side by side, and remember that toolsets are set per MCP client, so one config can serve differently trimmed clients.
# Google Cloud project setup
Source: https://docs.getadloop.com/self-hosting/google-cloud-project
Your own OAuth credentials in about five minutes.
Self-hosted AdLoop uses your own Google Cloud project: no shared user caps, no dependency on someone else's verification review, and the consent screen shows *your* project to *your* account. `adloop init` walks you through this; here's the same path for reference.
[console.cloud.google.com](https://console.cloud.google.com) → New project. Free; no billing account needed for these APIs.
Under **APIs & Services → Library**, enable: **Google Ads API**, **Google Analytics Data API**, and **Google Analytics Admin API**. Optionally, for the corresponding toolsets: **Tag Manager API**, **Search Console API**, **Merchant API**, **PageSpeed Insights API**.
**APIs & Services → Credentials → Create credentials → OAuth client ID**, application type **Desktop app**. Download the JSON; the wizard copies it to `~/.adloop/credentials.json`.
Add yourself as a test user, or publish the app to **In production** (recommended: tokens for apps left in "Testing" expire every 7 days, and Basic API access below requires it).
Since 9 September 2026, Google Ads API access belongs to the Cloud project that owns your OAuth client. Developer tokens are sunset and no Manager Account is needed. Open the project's [Google Ads API Overview page](https://console.cloud.google.com/google/ads-apis/overview) and click **Upgrade access level**:
| Level | How | Allows |
| - | - | - |
| Test | automatic once the API is enabled | test accounts only |
| Explorer | apply; usually granted automatically | 2,880 operations/day on production accounts |
| Basic | apply; reviewed automatically within minutes once the consent screen is External and In production | 15,000 operations/day |
| Standard | apply; manual review, about ten business days | higher limits, Google's RMF policy applies |
A project on a Cloud free trial or with billing disabled is refused Explorer and Basic.
`CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION` (or `DEVELOPER_TOKEN_NOT_APPROVED` on older API versions) means the project is still at Test level. Apply for Explorer access; nothing in AdLoop's configuration needs to change.
If your OAuth token keeps expiring weekly, your consent screen is still in "Testing". Publish it to "In production" in the Google Cloud Console.
## Adding scopes later
When AdLoop adds new Google services, existing tokens don't cover the new scopes. AdLoop detects this and triggers a fresh consent automatically; approve all requested permissions in one go. Details in [Troubleshooting](/troubleshooting).
# Troubleshooting
Source: https://docs.getadloop.com/troubleshooting
The errors you'll actually meet, and their fixes.
## First move: the health check
Ask your AI to run `health_check`. It tests the OAuth token, GA4, and Ads connectivity and returns actionable guidance per service. GA4 is checked on two surfaces: `ga4_admin` (listing properties) and `ga4_data` (every report). `ga4_data: "error"` with `SERVICE_DISABLED` means the Google Analytics Data API isn't enabled in your Google Cloud project (self-hosted); enable it there. It isn't a permissions problem.
## "This app isn't verified" (self-hosted)
Your own Google Cloud project has not been through Google's app review, so Google warns about it on the consent screen. Click **Advanced**, then continue to your project. This is expected for a project only you and your team use. AdLoop Cloud is verified by Google and shows no such warning.
## Authorization errors after an AdLoop update (self-hosted)
When new Google services are added, stored tokens don't cover the new scopes.
* AdLoop discards outdated tokens automatically and re-runs consent on the next tool call. **Approve all permissions in one go**: partially granted consents are rejected with an error naming the missing scopes, because a partial token would break every other service.
* If you see `invalid_scope` errors, delete `~/.adloop/token.json` and run any tool to re-authorize.
## Token expires every 7 days (self-hosted)
Your Google Cloud consent screen is in "Testing" status. Publish it to **In production** (Google Cloud Console → APIs & Services → OAuth consent screen).
## GTM or Search Console tools return permission errors
* The **Tag Manager API** / **Search Console API** must be enabled in your Google Cloud project (self-hosted).
* Your Google account needs at least read access to the GTM container / GSC property.
* Tokens created before these toolsets existed must re-consent once (see above).
## "This API key is limited to the toolsets…" (Cloud)
The key was created with a [toolset restriction](/concepts/toolsets) and the AI called a tool outside it. Use a key that includes the toolset, or create one in **Settings → API keys**. Retrying with the same key won't help.
## New tools don't show up in claude.ai (Cloud)
claude.ai lists a connector's tools once and caches them. After AdLoop adds tools (for example when you connect [Reddit Ads](/integrations/reddit-ads)), refresh the list under **Settings → Connectors → AdLoop**; Claude Code and Cursor pick new tools up on their next start. There is no server-side push for this: the MCP endpoint is stateless, so clients only re-list when they reconnect.
## "This workspace is on the solo plan…" (Cloud)
Not a permissions problem. Solo workspaces manage exactly one account pairing (one Ads account, one GA4 property, one Reddit ad account), and a call naming any other account is refused before it reaches Google or Reddit. Either change the workspace selection under **Settings → Google** / **Settings → Reddit**, or move to the agency plan, which adds client profiles and per-call account overrides so several accounts can be analyzed side by side.
## Ads clicks don't match GA4 sessions
Almost always EU consent behavior, not broken tracking. Read [Consent and data gaps](/concepts/consent-and-data) before changing anything.
## A write was refused
That's usually the [safety model](/concepts/safety-model) doing its job: budget above `max_daily_budget`, a bid raised beyond `max_bid_increase_pct`, broad match on manual CPC, missing geo/language targeting, or a read-only key hitting a write tool. The refusal message names the reason and where to change the setting.
## `DRY_RUN_FAILED`
The dry run checked the change with the ad platform and the platform rejected it. For Google Ads the error is Google's own, for example `[mutate_error=RESOURCE_NOT_FOUND] Resource was not found.` when the ad group no longer exists. The real apply would fail the same way, so fix the cause and draft the change again. On AdLoop Cloud (and self-hosted with `two_phase_apply`), a plan whose dry run failed can't be applied.
## "Your workspace reached its daily ad platform operation limit" (Cloud)
Google Ads and Reddit Ads calls count together against a daily limit that resets at midnight UTC. Analytics, Tag Manager, Search Console and Merchant Center keep working, and so does `health_check`. See [plans and limits](/cloud/plans-and-limits) for what counts and how much each plan includes.
## "Your workspace used its monthly ad platform operations" (Cloud)
On the Free plan, Google Ads and Reddit Ads calls also count against 250 operations per calendar month, reset on the 1st (UTC). The rest works as with the daily limit above; Pro has no monthly limit.