Zernio
APIFor you
Zernio
APIFor you
Platforms

Google Ads

Overview

Build

Create adsPerformance MaxDemand Gen

Target

TargetingKeywordsCustomer Match

Measure

Insights & GAQLConversionsGoogle TagURL tracking tags

Operate

Manage adsRecommendationsAccounts & labels

Reference

Limits and errors
Dashboard
OverviewPlatformsAPI ReferenceResources
Google Ads

Create ads

Create a Google Search, Display or Performance Max campaign in one POST /v1/ads/create call, edit RSA headlines and descriptions with pinning, and manage sitelink, callout and structured-snippet assets.


By the end of this page, a Google Search or Display campaign is live, with its ad group and ad, from one POST /v1/ads/create call.

Before you start

You need a googleads account and the customer id from List ad accounts.

Create a campaign

campaignType (lowercase search, display, pmax or demand_gen) selects the campaign type. Demand Gen has its own page.

This first example builds a Search campaign. Call POST /v1/ads/create with campaignType: "search", the ad copy and keywords.

TypeScript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: created } = await zernio.adcampaigns.createStandaloneAd({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    adAccountId: '1234567890',
    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']
  }
});

console.log(created.ad._id);

Response (201):

JSON
{
  "ad": {
    "_id": "66f0a1b2c3d4e5f6a7b8c9d0",
    "name": "US DevOps Search",
    "platform": "google",
    "status": "pending_review",
    "adType": "standalone",
    "goal": "traffic",
    "budget": { "amount": 50, "type": "daily" },
    "platformAdId": "735198246013",
    "platformCampaignId": "21874563210",
    "platformAdSetId": "165489732105"
  },
  "message": "Ad created"
}

platformAdSetId is the Google ad group id (Zernio's adSetId).

_id is what the Insights and tracking tag endpoints take as adId.

budgetAmount is whole units of the ad account's currency, not micros and not cents: 50 with budgetType: "daily" is $50.00 a day on a USD account.

Google's own API takes micros here, so a micros value copied from a GAQL report is a million times too large.

  • goal is engagement, traffic or awareness.
    • Google Ads rejects video_views at create with a 422 and code feature_not_available.
    • Conversion-goal campaigns are not supported on /v1/ads/create either. Pass traffic for click-optimised delivery.
  • keywords is a flat array of strings, created as broad-match keywords on the new ad group (up to 1,000 per call, 80 characters each).
    • An entry can also be { text, matchType } with exact, phrase or broad. Keywords covers editing them later.
  • countries and the other audience fields are on Targeting.
  • additionalHeadlines and additionalDescriptions apply to Search Responsive Search Ads only.
    • Each entry accepts either a string or an object with text and optional pinnedField.
    • pinnedField takes HEADLINE_1, HEADLINE_2 or HEADLINE_3 for headlines, and DESCRIPTION_1 or DESCRIPTION_2 for descriptions. For example, { "text": "Build your Dev Platform", "pinnedField": "HEADLINE_2" } pins an extra headline.
    • After including the primary text and deduplicating, the effective lists must contain 3 to 15 headlines and 2 to 4 descriptions. Excess entries return 400.

status is pending_review on every Google create. Later syncs re-derive it from Google:

statusWhen
activeThe ad is approved
rejectedGoogle disapproves it
pausedPaused
completedThe campaign has ended
cancelledThe ad is removed
errorGoogle reports a state Zernio does not recognise

Read GET /v1/ads/{adId} to see where it landed. The ad.status_changed webhook fires for Meta only.

Create a Display campaign

Call POST /v1/ads/create with campaignType: "display", businessName and both images.

TypeScript
const { data: display } = await zernio.adcampaigns.createStandaloneAd({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    adAccountId: '1234567890',
    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'
    },
    countries: ['US']
  }
});

console.log(display.ad.platformCampaignId);

Response (201), same shape as the Search create:

JSON
{
  "ad": {
    "_id": "66f0a1b2c3d4e5f6a7b8c9d1",
    "name": "Retargeting Display",
    "platform": "google",
    "status": "pending_review",
    "adType": "standalone",
    "goal": "traffic",
    "budget": { "amount": 40, "type": "daily" },
    "platformAdId": "735198246014",
    "platformCampaignId": "21874563211",
    "platformAdSetId": "165489732106"
  },
  "message": "Ad created"
}

Display campaigns require businessName (max 25 characters) on top of the Search fields.

longHeadline defaults to headline when omitted (max 90 characters).

Discovered campaigns of every type, including Shopping and Video, sync into GET /v1/ads/tree with metrics. /v1/ads/create builds Search, Display, Performance Max and Demand Gen.

Create a Performance Max campaign

Call POST /v1/ads/create with campaignType: "pmax" and an assetGroup.

The campaign, its budget and the asset group with all its assets are created in one atomic Google mutate. You either get the whole thing or nothing.

TypeScript
const { data: pmax } = await zernio.adcampaigns.createStandaloneAd({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    adAccountId: '1234567890',
    name: 'Spring Performance Max',
    campaignType: 'pmax',
    budgetAmount: 40,
    budgetType: 'daily',
    assetGroup: {
      businessName: 'Acme',
      finalUrl: 'https://example.com',
      longHeadline: 'Everything your team needs in one place',
      headlines: ['Acme for teams', 'Start in minutes', 'Built for scale'],
      descriptions: [
        'Set up in minutes and invite your whole team.',
        'Trusted by thousands of growing companies.'
      ],
      images: {
        landscape: ['https://cdn.example.com/pmax-1200x628.jpg'],
        square: ['https://cdn.example.com/pmax-1080x1080.jpg'],
        logo: ['https://cdn.example.com/logo-1200x1200.jpg']
      }
    },
    countries: ['US']
  }
});

The asset group needs:

  • businessName, finalUrl and longHeadline
  • at least three headlines
  • at least two descriptions
  • images under all three of landscape, square and logo

A YouTube video is optional via assetGroup.youtubeVideoId.

Read the result back with GET /v1/ads/campaigns/{campaignId}/asset-groups. Adding more asset groups, swapping assets and product targeting are on Performance Max.

Performance Max differs from Search and Display in ways that will reject the request rather than degrade quietly:

RuleDetail
Always created pausedGoogle rejects creating a Performance Max campaign in an active state. Activate it after review.
Daily budget onlybudgetType must be daily.
BiddingMaximize Conversions or Maximize Conversion Value. Portfolio bidding and bid caps are rejected.
TargetingGeo and language only. An omitted geo targets all locations.
No legacy creative fieldsheadline, body, linkUrl and attach shapes belong to the other campaign types.

Send validateOnly: true to validate the whole request against Google without creating or persisting anything. It is the safe way to check a payload before committing budget.

Edit responsive search ad text

PUT /v1/ads/{adId} replaces a Search ad's RSA text with top-level headlines, descriptions and finalUrls.

Each array is a full replacement of that list, not a merge. Omit a field entirely to leave it unchanged, or include it to replace every entry, including ones you want to keep.

TypeScript
const { data: updated } = await zernio.adcampaigns.updateAd({
  path: { adId: '66f0a1b2c3d4e5f6a7b8c9d0' },
  body: {
    headlines: [
      { text: 'Social Media API', pinnedField: 'HEADLINE_1' },
      { text: 'Schedule Your Posts' },
      { text: 'Build With Zernio' }
    ],
    descriptions: [
      { text: 'Connect social accounts and schedule posts with the Zernio API.', pinnedField: 'DESCRIPTION_1' },
      { text: 'Build social publishing into your application.' }
    ],
    finalUrls: ['https://zernio.com']
  }
});

Use 3 to 15 headlines (1 to 30 characters each) and 2 to 4 descriptions (1 to 90 characters each). Zernio does not pad or truncate on update the way it does on create.

pinnedField fixes an asset to one position (HEADLINE_1, HEADLINE_2, HEADLINE_3, DESCRIPTION_1 or DESCRIPTION_2).

  • Omit an asset from a supplied list to remove it.
  • Include it without pinnedField to unpin it.
  • Omit the entire list to preserve its current text and pins.

These Google updates use the top-level fields above. The legacy creative fields are unsupported for Google.

Read the current text and pins back with GET /v1/ads/{adId} in ad.creative.headlines, ad.creative.descriptions and ad.creative.finalUrls.

Google Search enrichment preserves pinnedField and reports top-level cachedAt and stale. Google mutations invalidate this read.

Enrichment failure. The endpoint still returns 200 with stored details and no cache metadata. That response is not confirmation of a fresh Google read.

Account, campaign and ad-group assets

Sitelinks, structured snippets and callouts are Google's account extensions. They exist at 3 levels, each with its own attach lifecycle:

LevelAttach withManage with
Account (reusable across campaigns)Add account sitelinks, snippets, calloutsList, update, remove
CampaignAttach campaign assetsList, update, remove
Ad group (ad set)Attach ad-group assetsList, update, remove

POST /v1/ads/campaigns/{campaignId}/assets and POST /v1/ads/ad-sets/{adSetId}/assets create and attach sitelinks, callouts and structuredSnippets in one mutation at the chosen level.

At all three levels, updates edit the underlying shared Google asset in place.

Send updates with assetResourceName and the fields to change. Omitted fields stay unchanged.

A change is visible on every attachment using that asset, even when you make it through a campaign or ad-group endpoint.

Updates count toward Zernio's per-minute burst limit (see Quotas and the ops budget) and invalidate affected cached lists.

Removing at any level only detaches the specified attachment. Google assets cannot be deleted, so the underlying asset and its other attachments remain.

Media requirements

TypeFormatMaxNotes
Responsive Search AdText15 headlines and 4 descriptionsHeadlines are truncated to 30 characters and descriptions to 90; Zernio pads up to Google's minimum of 3 headlines and 2 descriptions
Responsive Display (landscape)JPEG, PNG5120 KB1.91:1, 1200 x 628 recommended. Required.
Responsive Display (square)JPEG, PNG5120 KB1:1, 1080 x 1080 recommended. Required.
KeywordText80 charactersbroad, phrase or exact match

If it fails

ErrorCauseFix
400 with NOT_ENOUGH_SQUARE_MARKETING_IMAGE_ASSETOne of the 2 Display images is missingPass both images.landscape and images.square and repeat the call
422 feature_not_availablegoal: "video_views" on a createUse engagement, traffic or awareness
400 on additionalHeadlines or additionalDescriptionsAfter deduplicating, the lists hold more than 15 headlines or 4 descriptionsTrim the lists
400 on locationTargetingTypeThe request joins an existing campaignChange it on the campaign, see Targeting

Google's own message is forwarded in platformError:

JSON
{
  "error": "Google rejected the ad: NOT_ENOUGH_SQUARE_MARKETING_IMAGE_ASSET",
  "type": "platform_error",
  "code": "platform_api_error",
  "platform": "google",
  "platformError": {
    "code": 3,
    "message": "Request contains an invalid argument.",
    "details": [ { "errors": [ { "errorCode": { "assetError": "NOT_ENOUGH_SQUARE_MARKETING_IMAGE_ASSET" }, "message": "Too few." } ] } ]
  }
}

Error handling covers the envelope.

Related

  • Targeting: locations, languages, audiences and who a campaign reaches.
  • Keywords: read, add and research keywords.
  • Pause or resume: status, budget and delete once the campaign runs.
  • Limits and errors: quotas, what the API cannot do, and the errors Google returns most often.
  • Create standalone ad: every field, including sitelinks, callouts and structuredSnippets.
Was this page helpful?

Google Ads

Create Search, Display, Performance Max and Demand Gen campaigns, mine keywords, upload Customer Match lists, send conversions, manage goals, labels and MCC links, apply recommendations and query GAQL.

Performance Max

Manage a Google Performance Max campaign's asset groups: read them with ad strength, add groups, edit URLs and status, link and unlink assets, and replace the retail listing-group tree.

On this page

Before you startCreate a campaignCreate a Display campaignCreate a Performance Max campaignEdit responsive search ad textAccount, campaign and ad-group assetsMedia requirementsIf it failsRelated