Google Ads

Search campaigns without the gRPC nightmares. Type-safe builders, human-readable enums.

TypeScript
import { google, daily, exact, phrase,
  headlines, descriptions, rsa, url } from '@upspawn/ads'

export default google.search('Brand - Acme', {
  budget: daily(45),
  bidding: 'maximize-clicks',
})
  .group('core-keywords', {
    keywords: [...exact('acme'), ...phrase('ai workflow automation')],
    ad: rsa(
      headlines('Automate Any Workflow', 'AI-Powered', 'Free Trial'),
      descriptions('Connect 200+ apps. Ship workflows in minutes.'),
      url('https://acme.dev'),
    ),
  })
produces this ad
A
acme.dev· Sponsored
Automate Any Workflow | AI-Powered | Free Trial

Connect 200+ apps. Ship workflows in minutes.

Brand - Acme
└─ core-keywords
├─ acme [exact]
├─ ai workflow automation [phrase]
└─ RSA → 3 headlines, 1 description

The Google Ads gRPC API returns numeric enums everywhere. Status 2 means ENABLED. Bidding strategy 10 is TARGET_SPEND, which is actually Maximize Clicks. Good luck remembering that.

Budget is a separate resource that has to be created before the campaign that references it. The REST API is broken for mutations. Manually constructing operation arrays is tedious and error-prone.

ads-as-code wraps all of this: type-safe builders, human-readable enums, dependency-ordered mutations, and branded types that catch mistakes at construction time.

How it works

Campaigns and ad groups

Chain `.group()` calls to define ad groups with keywords and RSA ads. Each group can have its own keyword list, targeting locale, and ad.

TypeScript
import { google, daily, exact, phrase, broad,
  headlines, descriptions, rsa, url } from '@upspawn/ads'

google.search('EU Search - Acme', { budget: daily(80), bidding: 'maximize-conversions' })
  .group('DE — Core', {
    locale: { language: 'de', geo: ['DE', 'AT', 'CH'] },
    keywords: [...exact('acme'), ...phrase('ki workflow automatisierung')],
    ad: rsa(
      headlines('Acme — KI-Automatisierung', 'Workflows automatisch erstellen'),
      descriptions('Automatisieren Sie jeden Workflow mit KI. Kein Code nötig.'),
      url('https://acme.dev/de/'),
    ),
  })
  .group('EN — Core', {
    locale: { language: 'en', geo: ['GB', 'IE'] },
    keywords: [...exact('acme'), ...broad('workflow automation tool')],
    ad: rsa(
      headlines('Automate Any Workflow', '500+ Integrations, Zero Code'),
      descriptions('Build workflows with AI. Deploy in minutes.'),
      url('https://acme.dev/'),
    ),
  })

Extensions: sitelinks and callouts

Add sitelinks and callout extensions to your campaign. Extensions are versioned alongside your campaign definitions and diffed independently.

TypeScript
import { google, daily, exact, phrase, link, sitelinks, callouts } from '@upspawn/ads'

google.search('Brand - Acme', { budget: daily(45), bidding: 'maximize-clicks' })
  .sitelinks(sitelinks([
    link('Pricing',      'https://acme.dev/pricing'),
    link('Integrations', 'https://acme.dev/integrations'),
    link('Templates',    'https://acme.dev/templates'),
  ]))
  .callouts(callouts(['No-Code Required', 'Free 14-Day Trial', 'SOC 2 Certified']))
  .group('core-keywords', { /* ... */ })

Capabilities

Type-safe builders

`.search()`, `.group()`, `.ad()` — chained builders with TypeScript inference at every step.

Branded types

Headlines, descriptions, and callout text are branded strings. Constraints (e.g. ≤30 chars) are enforced at construction time.

Budget helpers

`daily(45)` or `monthly(1200)` — human-readable, converts to micros automatically.

All keyword match types

`exact()`, `phrase()`, `broad()` — the helper functions produce correctly-typed keyword objects.

Bidding strategies

`maximize-clicks`, `maximize-conversions`, `target-cpa` — human-readable aliases for gRPC numeric enums.

Sitelinks and callouts

Extensions are versioned alongside campaign definitions and diffed independently from ad changes.