Google Ads
Create Search and Display campaigns via Zernio API - No MCC or Standard Access application required
Included with the Usage plan. No MCC or Standard Access application needed. Zernio operates under our own approved developer token.
Display campaigns need two images. Google's Responsive Display Ads (RDA) require BOTH a landscape (1.91:1, e.g. 1200x628) AND a square (1:1, e.g. 1080x1080) marketing image. Sending only one is rejected upstream as "Too few." (NOT_ENOUGH_SQUARE_MARKETING_IMAGE_ASSET or NOT_ENOUGH_MARKETING_IMAGE_ASSET). Pass both via images: { landscape, square }.
What's Supported
| Feature | Status |
|---|---|
| Search campaigns (Responsive Search Ads) | Yes |
| Display campaigns (Responsive Display Ads) | Yes |
| Campaign > Ad Group > Ad hierarchy | Yes |
| Keyword targeting (Broad, Phrase, Exact) | Yes |
| Geo + language targeting | Yes |
| Real-time analytics (spend, CPC, CPM) | Yes |
| Conversions API (offline/enhanced) via Data Manager | Yes |
| Campaign URL tracking tags — read + update (details) | Yes |
| Performance Max | Roadmap |
| Shopping, Video, Demand Gen | Roadmap |
| Customer Match audiences (create + member upload) | Yes |
OAuth Scopes
Google Ads uses a dedicated OAuth connection (separate from YouTube or Google Business Profile) that requests two scopes:
| Scope | What it enables |
|---|---|
https://www.googleapis.com/auth/adwords | Full Google Ads API access: campaigns, ad groups, ads, reporting, conversions |
https://www.googleapis.com/auth/datamanager | Data Manager API: Customer Match audience uploads |
Create a Search Campaign
const ad = await zernio.ads.createStandaloneAd({ body: {
accountId: "acc_googleads_123",
adAccountId: "987-654-3210",
name: "US DevOps Search",
campaignType: "search",
goal: "traffic",
budgetAmount: 50,
budgetType: "daily",
headline: "Internal Developer Platforms",
body: "Spec-driven platform engineering.",
linkUrl: "https://example.com/platform",
keywords: ["platform engineering tools", "internal developer platform"],
additionalHeadlines: ["Build your Dev Platform", "Ship DevEx Faster", "Internal Platforms, Done Right"],
additionalDescriptions: ["Spec-driven platform. Free tier available.", "Enterprise features. Startup price."],
countries: ["US", "CA", "GB"],
}});ad = client.ads.create_standalone_ad(
account_id="acc_googleads_123",
ad_account_id="987-654-3210",
name="US DevOps Search",
campaign_type="search",
goal="traffic",
budget_amount=50,
budget_type="daily",
headline="Internal Developer Platforms",
body="Spec-driven platform engineering.",
link_url="https://example.com/platform",
keywords=["platform engineering tools", "internal developer platform"],
additional_headlines=["Build your Dev Platform", "Ship DevEx Faster", "Internal Platforms, Done Right"],
additional_descriptions=["Spec-driven platform. Free tier available.", "Enterprise features. Startup price."],
countries=["US", "CA", "GB"],
)curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "acc_googleads_123",
"adAccountId": "987-654-3210",
"name": "US DevOps Search",
"campaignType": "search",
"goal": "traffic",
"budgetAmount": 50,
"budgetType": "daily",
"headline": "Internal Developer Platforms",
"body": "Spec-driven platform engineering.",
"linkUrl": "https://example.com/platform",
"keywords": ["platform engineering tools", "internal developer platform"],
"additionalHeadlines": ["Build your Dev Platform", "Ship DevEx Faster", "Internal Platforms, Done Right"],
"additionalDescriptions": ["Spec-driven platform. Free tier available.", "Enterprise features. Startup price."],
"countries": ["US", "CA", "GB"]
}'Valid goal values for Google Ads: engagement, traffic, awareness, video_views. Conversion-goal campaigns aren't yet supported on /v1/ads/create, pass traffic for click-optimised delivery. campaignType is lowercase (search or display). keywords is a flat array of strings; match types default to broad.
Create a Display Campaign
const ad = await zernio.ads.createStandaloneAd({ body: {
accountId: "acc_googleads_123",
adAccountId: "987-654-3210",
name: "Retargeting Display",
campaignType: "display",
goal: "traffic",
budgetAmount: 40,
budgetType: "daily",
headline: "Finish your signup",
body: "Come back and upgrade today.",
linkUrl: "https://example.com/upgrade",
businessName: "Acme",
images: {
landscape: "https://cdn.example.com/retarget-1200x628.jpg",
square: "https://cdn.example.com/retarget-1080x1080.jpg",
},
additionalHeadlines: ["Finish your signup", "Get 30% off this week"],
additionalDescriptions: ["Come back and upgrade today."],
countries: ["US"],
}});ad = client.ads.create_standalone_ad(
account_id="acc_googleads_123",
ad_account_id="987-654-3210",
name="Retargeting Display",
campaign_type="display",
goal="traffic",
budget_amount=40,
budget_type="daily",
headline="Finish your signup",
body="Come back and upgrade today.",
link_url="https://example.com/upgrade",
business_name="Acme",
images={
"landscape": "https://cdn.example.com/retarget-1200x628.jpg",
"square": "https://cdn.example.com/retarget-1080x1080.jpg",
},
additional_headlines=["Finish your signup", "Get 30% off this week"],
additional_descriptions=["Come back and upgrade today."],
countries=["US"],
)curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "acc_googleads_123",
"adAccountId": "987-654-3210",
"name": "Retargeting Display",
"campaignType": "display",
"goal": "traffic",
"budgetAmount": 40,
"budgetType": "daily",
"headline": "Finish your signup",
"body": "Come back and upgrade today.",
"linkUrl": "https://example.com/upgrade",
"businessName": "Acme",
"images": {
"landscape": "https://cdn.example.com/retarget-1200x628.jpg",
"square": "https://cdn.example.com/retarget-1080x1080.jpg"
},
"additionalHeadlines": ["Finish your signup", "Get 30% off this week"],
"additionalDescriptions": ["Come back and upgrade today."],
"countries": ["US"]
}'Display campaigns require businessName (max 25 chars) on top of the Search fields. longHeadline defaults to headline if not supplied (max 90 chars). Both images.landscape and images.square are required, Google rejects RDA with only one.
Conversions API
Import offline conversions (deal closed, lead qualified, subscription renewed) into Google Ads via the Data Manager API ingestEvents method, the endpoint Google recommends for all new integrations as of December 2025 (replacing the legacy uploadClickConversions). Zernio uses the Google Ads account you already connected; no additional OAuth, no developer token to apply for.
The same endpoint handles both attribution paths:
- Click attribution, include a
gclid,gbraid, orwbraidfrom the originating ad click - Enhanced Conversions for Leads, include hashed email / phone when no click ID is available
PII is SHA-256 hashed server-side. For @gmail.com / @googlemail.com addresses, we strip dots and +suffix from the local part before hashing per Google's spec so john.doe+promo@gmail.com matches johndoe@gmail.com.
Discover available conversion actions
const { data } = await zernio.ads.listConversionDestinations({
path: { accountId: 'ACCOUNT_ID' },
});data = client.ads.list_conversion_destinations(account_id="ACCOUNT_ID")curl "https://zernio.com/api/v1/accounts/ACCOUNT_ID/conversion-destinations" \
-H "Authorization: Bearer YOUR_API_KEY"Returns every enabled conversion action across accessible customers. The id is the conversion action resource name (customers/{customerId}/conversionActions/{id}). The type field reflects the action's category (PURCHASE, LEAD, SIGN_UP, etc.), Google locks the event type to the conversion action, not the per-event eventName.
Send a conversion event
const result = await zernio.ads.sendConversions({ body: {
accountId: 'ACCOUNT_ID',
destinationId: 'customers/1234567890/conversionActions/987654321',
consent: { adUserData: 'GRANTED', adPersonalization: 'GRANTED' }, // required for EEA/UK
events: [{
eventName: 'Purchase',
eventTime: Math.floor(Date.now() / 1000),
eventId: 'order_abc_123', // dedup, mapped to transactionId
value: 129.99,
currency: 'USD',
user: {
email: 'customer@example.com',
phone: '+14155551234',
clickIds: { gclid: 'EAIaIQobChMI...' },
},
}],
}});result = client.ads.send_conversions(
account_id="ACCOUNT_ID",
destination_id="customers/1234567890/conversionActions/987654321",
consent={"adUserData": "GRANTED", "adPersonalization": "GRANTED"},
events=[{
"eventName": "Purchase",
"eventTime": int(time.time()),
"eventId": "order_abc_123",
"value": 129.99,
"currency": "USD",
"user": {
"email": "customer@example.com",
"phone": "+14155551234",
"clickIds": {"gclid": "EAIaIQobChMI..."},
},
}],
)curl -X POST "https://zernio.com/api/v1/ads/conversions" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "ACCOUNT_ID",
"destinationId": "customers/1234567890/conversionActions/987654321",
"consent": { "adUserData": "GRANTED", "adPersonalization": "GRANTED" },
"events": [{
"eventName": "Purchase",
"eventTime": 1744732800,
"eventId": "order_abc_123",
"value": 129.99,
"currency": "USD",
"user": {
"email": "customer@example.com",
"phone": "+14155551234",
"clickIds": { "gclid": "EAIaIQobChMI..." }
}
}]
}'EEA / UK consent
Google enforces strict consent signaling for European users under the February 2026 Data Manager restrictions. Pass consent at the request root with adUserData and adPersonalization set to GRANTED or DENIED. Omit the field for non-EEA requests where you don't have consent data; Google applies region defaults.
Deduplication
Pass a stable eventId on every event, Zernio maps it to Google's transactionId so repeated uploads of the same conversion are deduped.
Batching + reporting
Up to 2,000 events per request (Zernio chunks larger batches automatically). Conversions take up to 3 hours to appear in Google Ads reports. The response carries a requestId you can reference if conversions don't appear as expected.
URL tracking tags
Read or update a campaign's click-URL tracking: trackingUrlTemplate (the redirect/tracking template, must contain {lpurl}) and finalUrlSuffix (parse-only key=value params appended to the landing page, the part that survives parallel tracking). Operate at the campaign level via the unified ad endpoint, passing any ad in the campaign.
Read the current values:
const { data } = await zernio.ads.getAdTrackingTags({ path: { adId: 'AD_ID' } });
// { platform: 'google', level: 'campaign', trackingUrlTemplate: '{lpurl}?utm_source=google', finalUrlSuffix: 'utm_medium=cpc' }data = client.ads.get_ad_tracking_tags(ad_id="AD_ID")curl "https://zernio.com/api/v1/ads/AD_ID/tracking-tags" \
-H "Authorization: Bearer YOUR_API_KEY"Update either field (send only the ones you want to change; omit a field to leave it untouched, pass an empty string to clear it):
const { data } = await zernio.ads.updateAdTrackingTags({
path: { adId: 'AD_ID' },
body: {
trackingUrlTemplate: '{lpurl}?utm_source=google&utm_campaign={campaignid}',
finalUrlSuffix: 'utm_medium=cpc',
},
});data = client.ads.update_ad_tracking_tags(
ad_id="AD_ID",
tracking_url_template="{lpurl}?utm_source=google&utm_campaign={campaignid}",
final_url_suffix="utm_medium=cpc",
)curl -X PATCH "https://zernio.com/api/v1/ads/AD_ID/tracking-tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"trackingUrlTemplate": "{lpurl}?utm_source=google&utm_campaign={campaignid}",
"finalUrlSuffix": "utm_medium=cpc"
}'These reads and writes go through the Google Ads API, which is rate-limited by access tier. A one-off audit is fine; auditing large numbers of campaigns can hit the daily operation cap on Basic access. If you plan high-volume tag auditing, let us know and we'll make sure the underlying account is on the higher tier.
Customer Match audiences
Target a CRM list via Customer Match. Create an empty audience, then upload members. adAccountId is the Google Ads customer ID (no dashes). Email only on Google (any phone is ignored); values are SHA256-hashed server-side before upload.
// 1. Create the (empty) Customer Match audience
await zernio.adaudiences.createAdAudience({
body: {
accountId: 'acc_googleads_123',
adAccountId: '1234567890', // Google Ads customer ID
type: 'customer_list',
name: 'Newsletter subscribers',
},
});
// 2. Upload members to the returned audience id
await zernio.adaudiences.addUsersToAdAudience({
path: { audienceId: 'aud_abc123' }, // id from the create response
body: { users: [{ email: 'jane@example.com' }, { email: 'sam@example.com' }] },
});# 1. Create the (empty) Customer Match audience
client.ad_audiences.create_ad_audience(
account_id="acc_googleads_123",
ad_account_id="1234567890", # Google Ads customer ID
type="customer_list",
name="Newsletter subscribers",
)
# 2. Upload members to the returned audience id
client.ad_audiences.add_users_to_ad_audience(
audience_id="aud_abc123", # id from the create response
users=[{"email": "jane@example.com"}, {"email": "sam@example.com"}],
)# 1. Create
curl -X POST "https://zernio.com/api/v1/ads/audiences" \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{ "accountId": "acc_googleads_123", "adAccountId": "1234567890", "type": "customer_list", "name": "Newsletter subscribers" }'
# 2. Upload members (use the audience id from the create response)
curl -X POST "https://zernio.com/api/v1/ads/audiences/AUDIENCE_ID/users" \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{ "users": [{ "email": "jane@example.com" }, { "email": "sam@example.com" }] }'Media Requirements
| Type | Format | Max Size | Notes |
|---|---|---|---|
| Responsive Search Ad | Text | 15 headlines / 4 descriptions | 30/90 chars |
| Responsive Display (landscape) | JPEG, PNG | 5120 KB | 1.91:1, 1200x628 recommended. Required. |
| Responsive Display (square) | JPEG, PNG | 5120 KB | 1:1, 1080x1080 recommended. Required. |
| Keyword | Text | 80 chars | BROAD, PHRASE, EXACT |
Version Migrations
Google Ads API ships v22, v23, v24... twice a year with breaking changes. Zernio absorbs these at our layer, so your integration keeps working without code changes.