Zernio
Zernio
PlatformsGoogle Ads

Build

Create AdsPerformance MaxDemand Gen

Target

KeywordsCustomer Match

Measure

Insights & GAQLConversionsURL Tracking Tags

Operate

RecommendationsAccounts & LabelsLimits and errors
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Google Ads

Performance Max

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


When you finish this page you can run the asset groups of a Performance Max campaign after it exists: read each group with its ad strength, add new groups, change their URLs and status, swap assets in and out, and set which products a retail group advertises. You need a googleads account and a Performance Max campaign, created with campaignType: "pmax" or discovered from Google.

Every endpoint below lives under /v1/ads/campaigns/{campaignId}/asset-groups, where campaignId is Google's numeric campaign id (platformCampaignId on the created ad, or the id on GET /v1/ads/campaigns). Every write accepts validateOnly: true, which runs Google's validation and creates or changes nothing.

Read asset groups

GET /v1/ads/campaigns/{campaignId}/asset-groups lists the campaign's groups with their linked text, image and YouTube assets. Removed groups and links are excluded.

curl "https://zernio.com/api/v1/ads/campaigns/21874563299/asset-groups" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200), trimmed to one asset:

{
  "assetGroups": [
    {
      "id": "123456789",
      "resourceName": "customers/1234567890/assetGroups/123456789",
      "campaignId": "21874563299",
      "name": "Social publishing",
      "status": "ENABLED",
      "finalUrls": ["https://example.com"],
      "finalMobileUrls": [],
      "path1": null,
      "path2": null,
      "adStrength": "GOOD",
      "primaryStatus": "ELIGIBLE",
      "primaryStatusReasons": [],
      "assets": [
        { "resourceName": "customers/1234567890/assets/987654321", "fieldType": "HEADLINE", "status": "ENABLED", "text": "Schedule posts" }
      ]
    }
  ],
  "cachedAt": "2027-01-31T08:00:00.000Z",
  "stale": false
}

GET .../asset-groups/{assetGroupId} reads one group in the same shape plus listingGroupFilters, its retail product tree as flat nodes (empty when the group has none). adStrength is Google's POOR, AVERAGE, GOOD or EXCELLENT, and primaryStatus with primaryStatusReasons says why the group is or is not serving.

Reads are cached for 10 minutes and fall back to the last successful copy with stale: true when Google's quota runs out; every write on this page clears the cache. The asset group's status is independent of the campaign's: a group can be ENABLED while the campaign is paused and spends nothing. Enable the campaign with PUT /v1/ads/campaigns/{campaignId}/status.

Add an asset group

POST .../asset-groups adds a group to an existing Performance Max campaign. The group, any new assets, their links and an optional listing-group tree go to Google in one atomic request, so Google checks the asset minimums against the whole set and you get everything or nothing.

curl -X POST "https://zernio.com/api/v1/ads/campaigns/21874563299/asset-groups" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Running shoes",
    "finalUrls": ["https://example.com/shoes"],
    "assets": [
      { "fieldType": "HEADLINE", "text": "Shoes for every run" },
      { "fieldType": "LONG_HEADLINE", "text": "Lightweight running shoes for road and trail" },
      { "fieldType": "DESCRIPTION", "text": "Free returns on every order." },
      { "fieldType": "MARKETING_IMAGE", "imageUrl": "https://cdn.example.com/shoes-1200x628.jpg" },
      { "fieldType": "LOGO", "asset": "customers/1234567890/assets/352426364803" }
    ]
  }'

Response (201):

{
  "assetGroup": {
    "id": "123456790",
    "resourceName": "customers/1234567890/assetGroups/123456790",
    "campaignId": "21874563299"
  }
}
  • name (unique within the campaign) and finalUrls (1 to 10) are required. finalMobileUrls, path1 and path2 (15 characters each; path2 needs path1) are optional.
  • The group is created PAUSED unless you send status: "ENABLED".
  • Each assets entry names a fieldType (Google's role, such as HEADLINE, LONG_HEADLINE, DESCRIPTION, BUSINESS_NAME, MARKETING_IMAGE, SQUARE_MARKETING_IMAGE, PORTRAIT_MARKETING_IMAGE, LOGO, LANDSCAPE_LOGO or YOUTUBE_VIDEO) and exactly one source: asset (an existing asset id or resource name from the same ad account), text, imageUrl or youtubeVideoId (new content, created in the same request). Up to 100 entries.
  • listingGroupFilter sets the retail product tree at creation, in the shape described under product targeting.

Edit a group

PATCH .../asset-groups/{assetGroupId} changes name, status (ENABLED or PAUSED), finalUrls, finalMobileUrls, path1 or path2. Only the fields you send are written; null on path1 or path2 clears it.

curl -X PATCH "https://zernio.com/api/v1/ads/campaigns/21874563299/asset-groups/123456790" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "ENABLED", "finalUrls": ["https://example.com/sale"] }'

Response (200): { "assetGroupId": "123456790", "updated": ["status", "finalUrls"] }.

DELETE .../asset-groups/{assetGroupId} removes the group on Google. Removal is not reversible; pass ?validateOnly=true to check it first.

Link and unlink assets

POST .../asset-groups/{assetGroupId}/assets links and unlinks in one atomic request. Links are applied before unlinks, so swapping the last asset of a role never drops the group below Google's per-role minimum mid-request.

curl -X POST "https://zernio.com/api/v1/ads/campaigns/21874563299/asset-groups/123456790/assets" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "link": [{ "fieldType": "HEADLINE", "text": "Free shipping today" }],
    "unlink": [{ "fieldType": "HEADLINE", "asset": "customers/1234567890/assets/419573236135" }]
  }'

Response (200): { "assetGroupId": "123456790", "linked": 1, "unlinked": 1 }.

link entries take the same shape as assets on create; unlink entries take fieldType and the asset id or resource name, as the group read returns it. Unlinking removes the link only: the asset stays in the account library, because Google has no asset delete.

To replace whole roles on the group a campaign was created with (for example every headline at once), you can also send assetGroup on PUT /v1/ads/{adId}, where Zernio lists the asset group as the ad.

Product targeting

A retail Performance Max campaign (one linked to Merchant Center) chooses its products with a listing-group tree on each asset group. PUT .../asset-groups/{assetGroupId}/listing-group-filters replaces the whole tree: the current nodes are removed and the new ones created in one atomic request.

curl -X PUT "https://zernio.com/api/v1/ads/campaigns/21874563299/asset-groups/123456790/listing-group-filters" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tree": {
      "children": [
        { "dimension": { "productBrand": { "value": "Acme" } } },
        { "dimension": { "productBrand": {} }, "excluded": true }
      ]
    }
  }'

Response (200): { "assetGroupId": "123456790", "created": 3, "removed": 1 }.

tree is the root. A root with no children targets all products. A node with children becomes a subdivision: its children (at least 2) share one dimension and include exactly one everything-else node, which is the dimension with no value ("productBrand": {} above). excluded: true on a leaf excludes those products. Read the current tree back as flat nodes in listingGroupFilters on the group read.

Only Merchant Center (SHOPPING) trees are supported. A campaign with no Merchant Center link returns Google's LISTING_SOURCE_NOT_ALLOWED as a 400.

If it fails

StatusCauseFix
400Google refused the request (an asset minimum not met, a duplicate group name, LISTING_SOURCE_NOT_ALLOWED); Google's message is forwardedFix the named field. validateOnly: true reproduces the check without writing.
404The campaign or asset group is not visible to your API keyCheck the ids against the list read.
422The Google Ads connection needs reconnectingReconnect the account (Connect).
429The per-user burst limit or Google's quota (quotas)Retry after the window or details.resetsAt.
501The campaign is not on Google AdsUse a Google Performance Max campaign id.

Related

  • Create ads: create the campaign with its first asset group.
  • Create a Performance Max asset group and Replace an asset group's listing-group tree: every field.
Was this page helpful?

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.

Demand Gen

Create Google Demand Gen campaigns with image, video or carousel ads across YouTube, Discover and Gmail, grow them with new ad groups and ads, and edit creative, channels, audiences and targeting.

On this page

Read asset groupsAdd an asset groupEdit a groupLink and unlink assetsProduct targetingIf it failsRelated