# Demand Gen

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

By the end of this page, a Google Demand Gen campaign exists, paused, with one ad group and one ad, from one `POST /v1/ads/create` call.

You also know how to add ad groups and ads to it and edit them later.

## Before you start

You need a `googleads` [account](/platforms/google-ads#connect) and the customer id from [List ad accounts](/ad-accounts/list-ad-accounts).

## Create a campaign

Demand Gen serves on YouTube (in-stream, in-feed and Shorts), Discover, Gmail and Google's display partners.

Set `campaignType: "demand_gen"` and put the creative, channel and audience settings in `demandGen`. The request creates a daily budget, the campaign, one ad group and one ad in a single atomic Google request.

```bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adAccountId": "1234567890",
    "name": "Demand Gen launch",
    "campaignType": "demand_gen",
    "budgetAmount": 10,
    "budgetType": "daily",
    "countries": ["ES"],
    "languages": ["es"],
    "demandGen": {
      "finalUrl": "https://example.com",
      "businessName": "Acme",
      "headlines": ["Schedule posts", "One social API"],
      "descriptions": ["Publish and manage social content through one API."],
      "callToAction": "Learn more",
      "images": {
        "landscape": ["https://cdn.example.com/landscape-1200x628.png"],
        "square": ["https://cdn.example.com/square-1080x1080.png"],
        "logo": ["https://cdn.example.com/logo-512.png"]
      },
      "channels": ["youtube_in_feed", "youtube_shorts", "discover", "gmail"],
      "audience": {
        "userInterests": ["92948"],
        "ageRanges": [{ "min": 25, "max": 54 }]
      }
    }
  }'
```

The created ad carries Google's native `platformCampaignId`, `platformAdSetId` (the ad group) and `platformAdId`.

The campaign is always created paused. Enable it with [`PUT /v1/ads/campaigns/{campaignId}/status`](/ad-campaigns/update-ad-campaign-status) once you have reviewed it.

Send `validateOnly: true` to have Google validate the whole request without creating anything.

- `budgetType` is `daily`, in whole units of the account currency like every other Google create.
- Geo (`countries`, `regions`, `cities`, `zips`, `metros`) and `languages` are written on the ad group, because Demand Gen keeps them there rather than on the campaign.
- Bidding:
  - Maximize Conversions: omit `bidStrategy` (or send `LOWEST_COST_WITHOUT_CAP`).
  - Target CPA: `COST_CAP` plus `bidAmount`.
  - Target ROAS: `LOWEST_COST_WITH_MIN_ROAS` plus `roasAverageFloor`.
  - Target CPC: `LOWEST_COST_WITH_BID_CAP` plus `bidAmount`.
- `locationTargetingType` (`presence` or `presence_or_interest`) is set on the new campaign.

## Choose the ad format

The fields you send in `demandGen` pick one of three ad formats:

| Format | Selected by | Creative fields |
|---|---|---|
| Multi-asset image ad | Default | `headlines` (1 to 5, 40 characters), `descriptions` (1 to 5, 90 characters), `businessName`, optional `callToAction`, and `images.landscape` or `images.square` (optionally `portrait`) plus `images.logo` |
| Video responsive ad | `youtubeVideoIds` (1 to 5 existing YouTube video ids) | `headlines`, `longHeadlines` (1 to 5, 90 characters, required here), `descriptions`, `businessName`, and exactly one `images.logo` |
| Carousel ad | `carouselCards` (2 to 10 cards) | Exactly one headline, one description, one logo, `businessName` and optional `callToAction`; each card has its own `headline`, `images` and optional `finalUrl` and `callToAction` |

`finalUrl`, `businessName` (25 characters), `headlines`, `descriptions` and `images.logo` are always required.

Image sizes:

- landscape 1.91:1 (at least 600 x 314)
- square 1:1 (at least 300 x 300)
- portrait 4:5 (at least 480 x 600)
- logo 1:1 (at least 128 x 128)

A carousel ad looks like this:

```json
{
  "campaignType": "demand_gen",
  "demandGen": {
    "finalUrl": "https://example.com",
    "businessName": "Acme",
    "headlines": ["One social API"],
    "descriptions": ["Publish everywhere from one API."],
    "images": { "logo": ["https://cdn.example.com/logo-512.png"] },
    "carouselCards": [
      { "headline": "Schedule posts", "images": { "square": "https://cdn.example.com/card-1.png" } },
      { "headline": "Read analytics", "finalUrl": "https://example.com/pricing", "images": { "square": "https://cdn.example.com/card-2.png" } }
    ]
  }
}
```

Each card needs its own image (no two cards may share one). Every card should use the same image shape.

Card images are uploaded to the account's asset library before the campaign is created, `validateOnly` included. Google checks cards against existing images.

An identical image is reused, not duplicated.

## Channels and audiences

`demandGen.channels` sets the ad group's channel controls. Only the listed channels serve, out of `youtube_in_stream`, `youtube_in_feed`, `youtube_shorts`, `discover`, `gmail` and `display`.

Omit it to serve on all of them.

`demandGen.audience` builds a Google Audience and attaches it to the ad group. Send at least one dimension:

| Field | Values |
|---|---|
| `userLists` | Numeric Google user list ids, such as a [Customer Match](/platforms/google-ads/audiences) list (up to 20) |
| `userInterests` | Numeric Google interest category ids (up to 20) |
| `customAudiences` | Numeric Google custom audience ids (up to 20) |
| `ageRanges` | Objects with `min` (18, 25, 35, 45, 55 or 65) and optional `max` (24, 34, 44, 54 or 64; omit for no upper bound) |
| `genders` | `male`, `female`, `undetermined` |

All ids must belong to the same Google customer.

To reuse an Audience you already have, send its numeric id as `demandGen.audienceId` instead of `audience`.

## Add ad groups and ads

Grow an existing Demand Gen campaign with the same endpoint and `campaignType: "demand_gen"`:

- **A new ad group:** send `existingCampaignId` with the campaign's Google id.
  - The new ad group gets its own geo, languages, `channels` and `audience` from the request, plus its ad, in one atomic request. It is created paused.
  - `demandGen.adGroupName` names it (it defaults to the ad name).
- **A new ad in an ad group:** send `adSetId` with the ad group's Google id.
  - `demandGen` carries only the ad's creative. Ad group settings (geo, languages, `channels`, `audience`, `audienceId`, `adGroupName`) return `400`.
  - The new ad is created paused.

```json
{
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "adAccountId": "1234567890",
  "name": "Demand Gen, second audience",
  "campaignType": "demand_gen",
  "existingCampaignId": "23508518100",
  "countries": ["US"],
  "demandGen": {
    "adGroupName": "Returning visitors",
    "finalUrl": "https://example.com",
    "businessName": "Acme",
    "headlines": ["Schedule posts"],
    "descriptions": ["Publish everywhere from one API."],
    "images": {
      "square": ["https://cdn.example.com/square-1080x1080.png"],
      "logo": ["https://cdn.example.com/logo-512.png"]
    },
    "audience": { "userLists": ["123456"] }
  }
}
```

Neither shape takes budget, bidding or schedule fields. The campaign owns them, and sending them returns `400`.

`locationTargetingType` also belongs to the campaign and returns `400` here.

The target must be a Demand Gen campaign or ad group, otherwise the request returns `400`. Both shapes support `validateOnly`.

**Migrated from Discovery.** A campaign that still targets locations and languages on the campaign refuses them on a new ad group.

Geo and language fields return `400` there, and the new ad group follows the campaign's targeting.

## Edit creative, channels and audience

[`PUT /v1/ads/{adId}`](/ad-campaigns/update-ad) takes a top-level `demandGen` object and applies it in one atomic Google request.

Every field you send replaces that whole field. Fields you omit are kept.

```bash
curl -X PUT "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d2" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "demandGen": {
      "headlines": ["Schedule posts", "One social API"],
      "images": { "square": ["https://cdn.example.com/new-square.png"] },
      "audience": { "userInterests": ["92948"], "genders": ["female"] }
    }
  }'
```

- Which creative fields apply depends on the ad's format, read from Google. A field the ad does not take returns `422`.
  - Image ads take `headlines`, `descriptions`, `businessName`, `callToAction` and `images`.
  - Video ads take `headlines`, `longHeadlines`, `descriptions`, `businessName`, `youtubeVideoIds` and exactly one `images.logo`.
  - Carousel ads take one headline, one description, one logo, `businessName` and `callToAction`.
- A carousel's cards cannot be edited: create a new carousel ad instead.
- New images are uploaded as new Google assets. The previous ones stay in the account's asset library.
- `channels`, `audience` and `audienceId` change the ad's ad group, so they apply to every ad in it.
  - `audience` builds a new Google Audience and attaches it in place of the current one. The previous audience is detached, not deleted.
  - Ad groups of campaigns migrated from Discovery use ungrouped audience segments, and Google refuses an Audience on them.
- The Search and Display creative fields (`headlines` at the top level, `creative`, `assetGroup`) return `422` on a Demand Gen ad.
- `demandGen` returns `422` on any other channel.

## Edit targeting

Demand Gen keeps locations and languages on the ad group, so there are two ways to change them:

- `targeting` on [`PUT /v1/ads/{adId}`](/ad-campaigns/update-ad) takes locations, languages, `locationTargetingType` and `devices`. Locations and languages go to that ad's ad group and apply to every ad in it.
- [`PUT /v1/ads/campaigns/{campaignId}/targeting`](/ad-campaigns/update-campaign-targeting) writes locations and languages to the campaign's ad group when it has exactly one.
  - The response carries that `adGroupId`. The campaign-level `locations` and `languages` read back then stay empty.
  - With several ad groups the call returns `400` naming them. Edit each through an ad in that ad group.
  - `devices` and `locationTargetingType` stay campaign-level.

Campaigns migrated from Discovery that still target on the campaign keep being written there.

Budget, bidding and name are edited with the regular [campaign endpoints](/ad-campaigns/update-ad-campaign).

## Related

- [Create ads](/platforms/google-ads/create-ads): Search, Display and Performance Max on the same endpoint.
- [Customer Match](/platforms/google-ads/audiences): build the user lists `audience.userLists` takes.
- [Create standalone ad](/ad-campaigns/create-standalone-ad) and [Update ad](/ad-campaigns/update-ad): every field of `demandGen`.