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/adsThis 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 --help2. Initialize a project
mkdir my-ads && cd my-ads
ads initThis creates:
my-ads/
├── ads.config.ts # Project config (customer ID, account ID)
└── campaigns/ # Your campaign definitions live here3. Configure credentials
For Google Ads, create ~/.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:
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 googleThis 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 config5. Write your first campaign
Create 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 planThis 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 delete7. Apply changes
ads applyCreates 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-runNext steps
- Google Search Campaigns — full guide to the campaign DSL
- Google Ads Setup — detailed credential setup
- Meta Ads Setup — Meta API credentials
- CLI Reference — all commands and flags
- API Reference — all SDK exports
- Import existing campaigns — pull live campaigns into TypeScript