# 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.

By the end of this page, you can run the asset groups of a Performance Max campaign after it exists:

- Read each group with its ad strength.
- Add groups.
- Change URLs and status.
- Swap assets.
- Set which products a retail group advertises.

## Before you start

You need a `googleads` [account](/platforms/google-ads#connect) and a Performance Max campaign. Create it with [`campaignType: "pmax"`](/platforms/google-ads/create-ads#create-a-performance-max-campaign), or use one discovered from Google.

Every endpoint below lives under `/v1/ads/campaigns/{campaignId}/asset-groups`. `campaignId` is Google's numeric campaign id (`platformCampaignId` on the created ad, or the id on [`GET /v1/ads/campaigns`](/ad-campaigns/list-ad-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`](/ad-campaigns/list-google-asset-groups) lists the campaign's groups with their linked text, image and YouTube assets. Removed groups and links are excluded.

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

Response (`200`), trimmed to one asset:

```json
{
  "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}`](/ad-campaigns/get-google-asset-group) reads one group in the same shape, plus `listingGroupFilters`. That is its retail product tree as flat nodes (empty when the group has none).

`adStrength` is Google's `POOR`, `AVERAGE`, `GOOD` or `EXCELLENT`. `primaryStatus` with `primaryStatusReasons` says why the group is or is not serving.

Reads are cached for 10 minutes. They fall back to the last successful copy with `stale: true` when Google's quota runs out. Every write on this page clears the cache.

<Callout type="warn">
The asset group's `status` is independent of the campaign's. A group can be `ENABLED` while the campaign is paused and spends nothing.
</Callout>

Enable the campaign with [`PUT /v1/ads/campaigns/{campaignId}/status`](/ad-campaigns/update-ad-campaign-status).

## Add an asset group

[`POST .../asset-groups`](/ad-campaigns/create-google-asset-group) 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. Google checks the asset minimums against the whole set, and you get everything or nothing.

```bash
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`):

```json
{
  "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: `HEADLINE`, `LONG_HEADLINE`, `DESCRIPTION`, `BUSINESS_NAME`, `MARKETING_IMAGE`, `SQUARE_MARKETING_IMAGE`, `PORTRAIT_MARKETING_IMAGE`, `LOGO`, `LANDSCAPE_LOGO` or `YOUTUBE_VIDEO`.
- Each entry also takes 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](#product-targeting).

## Edit a group

[`PATCH .../asset-groups/{assetGroupId}`](/ad-campaigns/update-google-asset-group) 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.

```bash
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}`](/ad-campaigns/remove-google-asset-group) removes the group on Google. Removal is not reversible, so pass `?validateOnly=true` to check it first.

## Link and unlink assets

[`POST .../asset-groups/{assetGroupId}/assets`](/ad-campaigns/edit-google-asset-group-assets) links and unlinks in one atomic request.

Links are applied before unlinks. Swapping the last asset of a role never drops the group below Google's per-role minimum mid-request.

```bash
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), send `assetGroup` on [`PUT /v1/ads/{adId}`](/ad-campaigns/update-ad). Zernio lists that 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`](/ad-campaigns/replace-google-listing-group-filters) replaces the whole tree. The current nodes are removed and the new ones created in one atomic request.

```bash
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: 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

| Status | Cause | Fix |
|---|---|---|
| `400` | Google refused the request (an asset minimum not met, a duplicate group name, `LISTING_SOURCE_NOT_ALLOWED`); Google's message is forwarded | Fix the named field. `validateOnly: true` reproduces the check without writing. |
| `404` | The campaign or asset group is not visible to your API key | Check the ids against the list read. |
| `422` | The Google Ads connection needs reconnecting | Reconnect the account ([Connect](/platforms/google-ads#connect)). |
| `429` | The per-user burst limit or Google's quota ([quotas](/platforms/google-ads/reference#quotas-and-the-ops-budget)) | Retry after the window or `details.resetsAt`. |
| `501` | The campaign is not on Google Ads | Use a Google Performance Max campaign id. |

## Related

- [Create ads](/platforms/google-ads/create-ads#create-a-performance-max-campaign): create the campaign with its first asset group.
- [Create a Performance Max asset group](/ad-campaigns/create-google-asset-group) and [Replace an asset group's listing-group tree](/ad-campaigns/replace-google-listing-group-filters): every field.