URL: /code/integrations/google-ads · Id: `page:/code/integrations/google-ads`

# Google Ads

> Your agents forecast your terms, build exact-match search campaigns on your Google Ads account, cut wasted search terms daily, and stop by your rule.

Your agents forecast your terms, build exact-match search campaigns on your Google [Ads](https://startupbuilder.dev/business/nouns/ad) account, cut wasted search terms daily, and stop by your rule.

```text title="Run it · Ask"
Which search terms spent over $20 last week with no signup? Show me what to add as negatives.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Metric.query({ metric: 'ad-spend', shape: 'trend', breakdown: 'search-term', window: '7d', where: 'signups = 0 and spend > 20' })
```

```json title="Run it · Response"
{
  "$type": "Metric",
  "mode": "try",
  "stored": false,
  "metric": "ad-spend",
  "window": "7d",
  "rows": [
    {
      "searchTerm": "free rent receipt template",
      "spend": 64.2,
      "clicks": 31,
      "signups": 0,
      "action": "add as a negative"
    },
    {
      "searchTerm": "property management software enterprise",
      "spend": 48.9,
      "clicks": 9,
      "signups": 0,
      "action": "add as a negative"
    }
  ]
}
```

- **Connect:** Sign in once, 1 grant
- **Runs through:** [Your account](/code/integrations#three-ways)
- **First run:** Forecast clicks and cost for 10 exact terms, about 90¢
- **Sites:** [Google Search](/venues#google-search), [Google Display](/venues#google-display), [YouTube](/venues#youtube)

## Connect once
1. Call `Integration.connect` with `system: 'ads'` and `venue: 'google-search'`, then open the `connectUrl` it returns and sign in as a Google account with access to your Google Ads account.
2. Pick the Google Ads account, then link it to your Google Analytics property so signups count as conversions.
3. Set the spend on your standing approval with `Mandate.authorize`; Google bills your account, never the platform.

| Grant | What it unlocks | Scope |
| --- | --- | --- |
| Manage your Google Ads account | Reports, campaigns, ad groups, keywords, ads, budgets and audiences; Google offers no read-only grant, so your standing approval holds what changes | `https://www.googleapis.com/auth/adwords` |

| To | Do this |
| --- | --- |
| Pause | `Integration.pause` stops every call through Google at once and keeps the token; `Integration.resume` picks up from the same cursor. |
| Reconnect | `Integration.reconnect` returns a fresh `connectUrl` that lasts one hour, and keeps the Integration's id and cursor, so nothing imports twice. |
| Disconnect | `Integration.revoke` revokes the token with Google; every record it brought in stays in your records. |

## What your agents do
| Ask | Verb | Runs through | Price |
| --- | --- | --- | --- |
| Build the search campaign: 4 ad groups of exact terms, two ads each, and my negatives. | [`Campaign.create`](/code/apps/ads/create-campaign) | Your account | Free |
| Add an ad to the best ad group with the winning headline. | [`Ad.create`](/code/apps/ads/create-ad) | Your account | Free |
| Add the search terms that wasted money as negatives, and lower the budget to $30 a day. | [`Campaign.update`](/code/apps/ads/update-campaign) | Your account | Free |
| Which search terms spent over $20 last week with no signup? | [`Metric.query`](/code/apps/analytics-and-experiments/query) | Your account | Free |
| Build a remarketing list from pricing-page visitors. | [`Audience.create`](/code/apps/ads/create-audience) | Your account | Free |

Calls on your own account are free; Google bills your spend directly.

Reading this site’s public data needs no account: [`Keyword.forecast`](/code/data/keywords/forecast), [`Ad.find`](/code/data/ads/find) are Data calls (the [three ways a call runs](/code/integrations#three-ways)).

## What it holds
| Noun | The account’s object | When | Which side leads |
| --- | --- | --- | --- |
| [Campaign](https://startupbuilder.dev/business/nouns/campaign) | `campaign and campaign_budget` | Your agent builds it | Your records |
| [Ad](https://startupbuilder.dev/business/nouns/ad) | `ad_group_ad` | Your agent builds it; Google’s review sets its status | Your records |
| [Keyword](https://startupbuilder.dev/business/nouns/keyword) | `search_term_view` | A search triggers your ad | The account |
| [Audience](https://startupbuilder.dev/business/nouns/audience) | `user_list` | Your agent builds one | Your records |
| [Entry](https://startupbuilder.dev/business/nouns/entry) | `metrics.cost_micros` | Each day’s spend settles | The account |

## Events and webhooks
| Direction | Event |
| --- | --- |
| Emits | `Campaign.created`, `Campaign.updated`, `Ad.created`, `Audience.created`, `Keyword.measured`, `Entry.posted` |

Any Event can wake a Workflow, and a Watch delivers it to your endpoint, signed ([Events and webhooks](/code/events)).

## Limits
| What | Limit | Call |
| --- | --- | --- |
| A day’s spend | Google may spend up to twice the daily budget on one day and holds the month to about 30.4 days of it, so your standing approval’s cap is set on the month | [`Campaign.update`](/code/apps/ads/update-campaign) |
| Ad review | Google reviews every ad; a disapproval comes back as `venue-rejected` with the policy it names | [`Ad.create`](/code/apps/ads/create-ad) |
| Smart Bidding | Learns from your conversion volume, and a new account has none, so a first test runs on manual CPC or maximize clicks on exact terms | [`Campaign.create`](/code/apps/ads/create-campaign) |
| Spend | Your standing approval holds the cap; a budget change past it waits with `over-mandate` | [`Campaign.update`](/code/apps/ads/update-campaign) |

## Start here
| Move | The rule |
| --- | --- |
| Structure | One search campaign, 3 to 5 ad groups of tightly themed exact terms, two responsive search ads per group, and the negatives in place on day 0. |
| Budget | Spend what 500 clicks cost at your terms’ cost per click, on manual CPC or maximize clicks, and only on terms whose cost per click sits under your break-even. |
| Creative | The search term in headline 1, the outcome in headline 2, and a landing page whose H1 repeats the term with one call to action. |
| Kill rule | At 500 clicks: stop if cost per signup at the interval’s upper bound still fails break-even, hold and rewrite for 300 more if only the lower bound fails, and scale one tier only when the lower bound passes. |

| Benchmark | Source |
| --- | --- |
| CTR 6.66%, cost per click $5.26, conversion 7.52%, cost per lead $70.11 | Study of 16,000+ advertisers, 2025 |
| Judge a bidding change over 2 conversion cycles or 2 weeks, whichever is longer | Google Ads help |
| 10 signups in 500 clicks reads as 2%, with an interval of 1.1% to 3.6% | Our calculation (Wilson interval) |
| No signup in 150 clicks stops the test: a 2% rate would show one 95% of the time | Our calculation |

## Field map
| Our field | Google Ads’s field | Notes | Calls |
| --- | --- | --- | --- |
| `Campaign.objective` | `campaign.advertising_channel_type` | With a conversion goal; `leads` runs a SEARCH campaign bidding toward your signup conversion | [`Campaign.create`](/code/apps/ads/create-campaign), [`Campaign.update`](/code/apps/ads/update-campaign), [`Ad.create`](/code/apps/ads/create-ad) |
| `Campaign.budget` | `campaign_budget.amount_micros` | A daily amount in millionths: $40 is 40000000 | [`Campaign.create`](/code/apps/ads/create-campaign), [`Campaign.update`](/code/apps/ads/update-campaign), [`Ad.create`](/code/apps/ads/create-ad) |
| `Campaign.startsAt` | `campaign.start_date` | None. | [`Campaign.create`](/code/apps/ads/create-campaign) |
| `Campaign.endsAt` | `campaign.end_date` | None. | [`Campaign.create`](/code/apps/ads/create-campaign) |
| `Campaign.keywords` | `ad_group_criterion.keyword` | match_type EXACT for a test | [`Campaign.create`](/code/apps/ads/create-campaign), [`Campaign.update`](/code/apps/ads/update-campaign) |
| `Campaign.negatives` | `campaign_criterion.negative` | With the wasted search term as its keyword | [`Campaign.create`](/code/apps/ads/create-campaign), [`Campaign.update`](/code/apps/ads/update-campaign) |
| `Ad.headline` | `ad_group_ad.​ad.​responsive_search_ad.​headlines` | 3 to 15, each 30 characters | [`Ad.create`](/code/apps/ads/create-ad) |
| `Ad.body` | `ad_group_ad.​ad.​responsive_search_ad.​descriptions` | 2 to 4, each 90 characters | [`Ad.create`](/code/apps/ads/create-ad) |
| `Ad.landingUrl` | `ad_group_ad.ad.final_urls` | None. | [`Ad.create`](/code/apps/ads/create-ad) |

## What try does here
Sends every change with `validate_only` set: Google Ads checks the whole build and returns any error, and creates nothing.

## How long a connection lasts
A connection lasts until you revoke it in your Google Account; Google also ends a grant left unused for six months, and every call your agents make counts as use.

The platform asks you to reconnect 7 days before it runs out; until you do, a call that needs it refuses with [`needs-reauth`](/code/refusals#needs-reauth).

## Formats
| Site | Format | Size | Limits |
| --- | --- | --- | --- |
| [Google Search](/venues#google-search) | Responsive search ad | None. | 3 to 15 headlines of 30 characters; 2 to 4 descriptions of 90; two paths of 15 |
| [YouTube](/venues#youtube) | Video | None. | Title 100 characters; description 5,000 bytes; tags 500 characters in all |

## Plays and strategies
| Kind | Where |
| --- | --- |
| Plays | [Paid search](/plays/paid-search), [Retargeting](/plays/retargeting), [Promote winners](/plays/promote-winners), [Positioning tournament](/plays/positioning-tournament) |
| Strategies | [Paid](/strategy/paid) |

## Used by
- **Workflows:** [Search readout](/capabilities/measure/search-readout) · [Ad test setup](/capabilities/run-ads/ad-test-setup)
- **Agent tasks:** [Message test](/capabilities/run-ads/message-test)

## Reference

| Call | Path | Price |
| --- | --- | --- |
| [`Campaign.create`](/code/apps/ads/create-campaign) | `POST /campaigns/create` | Free |
| [`Ad.create`](/code/apps/ads/create-ad) | `POST /ads/create` | Free |
| [`Campaign.update`](/code/apps/ads/update-campaign) | `POST /campaigns/update` | Free |
| [`Metric.query`](/code/apps/analytics-and-experiments/query) | `POST /metrics/query` | Free |
| [`Audience.create`](/code/apps/ads/create-audience) | `POST /audiences/create` | Free |