Skip to main content

Google Ads

The Google Ads connector reaches https://googleads.googleapis.com/v26, the Google Ads API, as one Google login. Almost everything you would open the Google Ads UI to look up is one GAQL query away: what is running, what each campaign spent yesterday, which keywords convert, what changed last week. It can also create, change, pause and remove campaigns, budgets, ad groups, ads and targeting, and every one of those ships switched off and asks a human before it runs, because each can start or change spend.

What you need​

Three things, and all three are yours rather than Permaura's.

1. A Google OAuth client. Google Ads is OAuth-only. Register a client the way the Google Drive page walks through: a Cloud console project, the consent screen, an OAuth client id and secret, and the redirect URI http://127.0.0.1:7376/oauth/callback for a gateway on your own machine or https://<your-gateway-host>/oauth/callback for a tunneled or hosted one. Enable the Google Ads API on the project. The one scope is https://www.googleapis.com/auth/adwords; it covers reads and writes alike, and the gateway's per-action switches are what keep the writes off.

An app left in Testing breaks every seven days

A refresh token issued while the OAuth app's publishing status is Testing expires seven days after issuance, and refreshing it does not extend that. Set the app's user type to Internal (Google Workspace only) or publish it to In production; unverified is fine for your own use. The Drive page has the detail.

2. A developer token. Every Google Ads API call carries a developer-token header. You get one from the API Center of a Google Ads manager account (Tools and settings, Setup, API Center); a plain advertiser account has no API Center, so create a manager account and link your advertiser account under it if you have none. A new token has test account access and can only reach test accounts; apply for Basic access from the same page to read and change real accounts. Google reviews that application and it can take a few days.

3. The customer ids. A Google Ads account id is ten digits, shown as 123-456-7890 in the UI and sent as 1234567890, no dashes. Every action takes the account's id as customer_id. When the signed-in user reaches the account through a manager account, Google also wants that manager's id in a login-customer-id header; when the user is a direct member of the advertiser account, leave it out. customers.accessible lists the ids the signed-in user can reach directly, which is a good first call.

Add the connection​

In the console, open Access, Connections, Add connection, pick Google Ads from the catalog, and fill in the Developer token field and, if you go through a manager account, the Login customer id. Under the credential choose OAuth, then Gateway-held · your own app, paste the client id and secret, check the redirect URI matches the one on your Google client, and choose Add. Google's consent page opens in a new tab; approve it as a user with access to the account, and your browser lands on the gateway's "Connected" page once the tokens are sealed.

The same flow runs against the gateway's REST API. The two headers travel as static_headers on the from-preset call:

BASE=http://127.0.0.1:7376
curl -s -X POST $BASE/v1/connections/from-preset \
-H @<(printf 'authorization: Bearer %s\n' "$TOKEN") -H 'content-type: application/json' -d '{
"preset_id": "google-ads",
"name": "ads-acme",
"static_headers": { "developer-token": "<your-developer-token>", "login-customer-id": "1234567890" }
}'

Then start the authorization against the returned connection_id exactly as on the Reddit page, with your Google client id and secret. The gateway refuses a from-preset call for this connector that carries no developer-token, since no call could succeed without it.

Where the developer token lives​

The gateway normally refuses any per-connection header whose name looks like a credential, and developer-token does. The catalog's Google Ads definition declares the header as one every connection sends, and the gateway accepts a declared header for that connector alone. Here is what that means for you:

  • The developer token is stored on the connection record on your gateway, in the clear, beside the pinned host, and anyone holding your gateway's operator token can read it there. Agents never see it; headers are not part of what a tool call returns.
  • The token identifies your API access to Google. On its own it reaches nothing: every call also needs the OAuth access token of a user with access to the account, and that token is what the gateway seals and refreshes. A leaked developer token lets someone else spend your API quota under your name, which is worth rotating for, not an account takeover.
  • Rotating it is a new connection, or a from-preset call with the new value, since the connection's headers are fixed when it is made.

If that is not the custody you want for it, hold off on this connector; the alternative, sealing the token into the vault beside the OAuth bundle, is a bigger change to the gateway's invoke path and is not built.

What the agent can do​

ActionWhat it doesOn by default
customers.accessibleLists the customer ids the signed-in user can reach directlyYes
searchRuns a GAQL query, one page at a timeYes
search_streamRuns a GAQL query and returns every row at onceYes
keyword_ideas.generateGenerates keyword ideas with volume and competition from seeds or a URLYes
campaign_budgets.mutateCreates, changes or removes campaign budgetsNo, and needs approval
campaigns.mutateCreates, changes, pauses, enables or removes campaignsNo, and needs approval
ad_groups.mutateCreates, changes, pauses, enables or removes ad groups and their bidsNo, and needs approval
ad_group_ads.mutateCreates, changes, pauses, enables or removes adsNo, and needs approval
ad_group_criteria.mutateCreates, changes or removes keywords and other ad group targetingNo, and needs approval
campaign_criteria.mutateCreates, changes or removes campaign targeting such as locations and negative keywordsNo, and needs approval
mutateApplies a batch of operations across any resource types in one callNo, and needs approval

Good to know​

  • Reads are POSTs, and that is fine. The Google Ads API takes a GAQL query in a JSON body, so Permaura's classifier would call search, search_stream and keyword_ideas.generate writes and switch them off. The catalog marks the three as reads at medium risk so the connector answers questions the moment it is connected. Nothing they do changes the account.
  • One query language for everything. search takes customer_id and query, for example SELECT campaign.id, campaign.name, campaign.status, metrics.cost_micros FROM campaign WHERE segments.date DURING LAST_7_DAYS. Costs come back in micros (divide by a million). The response carries nextPageToken; pass it back as pageToken. search_stream returns every row in one response and suits exports; search suits a conversation. Google's query builder lists the resources and fields.
  • Every change is a mutate. Each *.mutate action takes operations, a list where each item is a create, an update with an updateMask, or a remove naming a resource. Pausing a campaign is an update setting status: PAUSED with updateMask: status; enabling it is the same call with ENABLED, and both ask for an approval, since the second one starts spending. validateOnly: true runs Google's checks without applying anything, which is a good thing for an agent to do before it asks a human to approve the real call.
  • mutate is the widest write. googleAds:mutate takes mutateOperations across any resource types in one atomic call. It is classified critical, above the per-resource mutates, so a policy can allow campaign changes without allowing everything.
  • Customer ids are digits only, in the path and in login-customer-id. Google answers a dashed id with a 400 that names the id format.
  • The API version is pinned. The connector pins v26, the current version as of October 2026. Google releases new versions several times a year and retires each roughly a year after it ships; when v26 is retired, the Use a different host option on the connection form takes https://googleads.googleapis.com/v27 (or the version of the day) without a gateway update, because the host stays the same.
  • Personal fields are redacted. The email_address and inviter_user_email_address fields on customer user access rows are blanked before the agent sees them.
  • A 403 on the first call is usually the developer token's access level, not a Permaura refusal: a test-account token against a real account, or Basic access not yet granted. The error body names DEVELOPER_TOKEN_NOT_APPROVED or DEVELOPER_TOKEN_PROHIBITED when that is the cause.