ads-as-code

Getting Started

Install ads-as-code, authenticate with your ad platform, write your first campaign, and apply it.

This guide walks you through a complete setup in about 5 minutes.

What you'll need

  • Bun ≥ 1.0 — the SDK and CLI run on Bun
  • A Google Ads or Meta Ads account with API access enabled
  • API credentials for your platform — see Google Ads Setup or Meta Ads Setup for step-by-step instructions

1. Install

bun add @upspawn/ads

This adds the SDK as a project dependency and makes the ads CLI available via bunx ads (or install globally with bun add -g @upspawn/ads to use ads directly).

bunx ads --help

2. Initialize a project

mkdir my-ads && cd my-ads
ads init

This creates:

my-ads/
├── ads.config.ts       # Project config (customer ID, account ID)
└── campaigns/          # Your campaign definitions live here

3. Configure credentials

For Google Ads, create ~/.ads/credentials.json:

~/.ads/credentials.json
{
  "google_client_id": "YOUR_CLIENT_ID",
  "google_client_secret": "YOUR_CLIENT_SECRET",
  "google_refresh_token": "YOUR_REFRESH_TOKEN",
  "google_developer_token": "YOUR_DEVELOPER_TOKEN",
  "google_customer_id": "YOUR_CUSTOMER_ID",
  "google_manager_id": "YOUR_MANAGER_ID"
}

Then set your account IDs in ads.config.ts:

ads.config.ts
import { defineConfig } from '@upspawn/ads'

export default defineConfig({
  google: {
    customerId: 'YOUR_CUSTOMER_ID',
    managerId: 'YOUR_MANAGER_ID',
  },
})

See Google Ads Setup for step-by-step instructions on obtaining these credentials. For Meta Ads, see Meta Ads Setup.


4. Authenticate

ads auth google

This runs an OAuth flow, saves your refresh token, and verifies the connection to the Google Ads API. You should see the connected account name and ID.

ads doctor   # sanity-check credentials and config

5. Write your first campaign

Create campaigns/brand.ts:

campaigns/brand.ts
import {
  google,
  daily,
  targeting,
  geo,
  languages,
  exact,
  phrase,
  negatives,
  headlines,
  descriptions,
  rsa,
  url,
  link,
  callouts,
} from '@upspawn/ads'

export default google.search('Brand', {
  budget: daily(20),
  bidding: 'maximize-clicks',
  targeting: targeting(geo('US'), languages('en')),
  negatives: negatives('free', 'open source'),
})
  .group('brand-en', {
    keywords: [
      ...exact('my product', 'my product app'),
      ...phrase('my product for teams'),
    ],
    ad: rsa(
      headlines(
        'My Product — Official',
        'Start Free Today',
        'No Credit Card Required',
      ),
      descriptions(
        'The fastest way to get X done. Set up in minutes.',
        'Free plan available. Connect your tools and get started.',
      ),
      url('https://example.com'),
    ),
  })
  .sitelinks(
    link('Pricing', 'https://example.com/pricing', {
      description1: 'Free plan available',
      description2: 'No credit card required',
    }),
    link('Features', 'https://example.com/features'),
  )
  .callouts('Free Plan', 'No Setup Fees', 'Cancel Anytime')

6. Preview changes

ads plan

This fetches live state from Google Ads, diffs it against your TypeScript definitions, and shows what will be created, updated, or deleted. No changes are made.

Discovering campaigns... 1 found

Fetching live state from Google Ads...

  + campaign   Brand
  + adGroup    Brand / brand-en
  + keyword    Brand / brand-en / kw:my-product:EXACT
  + keyword    Brand / brand-en / kw:my-product-app:EXACT
  + keyword    Brand / brand-en / kw:my-product-for-teams:PHRASE
  + ad         Brand / brand-en / rsa:a3f2b1...
  + sitelink   Brand / sitelink:pricing
  + sitelink   Brand / sitelink:features

Summary: 8 to create, 0 to update, 0 to delete

7. Apply changes

ads apply

Creates or updates resources in the correct dependency order: campaign → ad group → keywords → ad → extensions.

To do a dry run without making any changes:

ads apply --dry-run

Next steps

On this page