Ad sets
An ad set holds the price, surfaces and geo targets for the ads in it.
The ad set object
{
"id": "adset_8271",
"object": "ad_set",
"campaign_id": "cmpn_412",
"external_id": "line-77",
"name": "Plumbing, Dallas-Fort Worth",
"status": "active",
"effective_status": "active",
"serving_issues": [],
"fixed_cpm_micros": 30000000,
"fixed_cpc_micros": null,
"targeting": {
"surfaces": ["ai", "static", "ugc"]
},
"context_hints": ["water heater repair", "burst pipe"],
"created_time": 1791590400,
"updated_time": 1791590400
}| Field | Type | Rules |
|---|---|---|
campaign_id | string | Required on create. cmpn_412 or 412. Cannot change. |
name | string | Required on create. Trimmed, then 3 to 256 characters with no control characters. |
external_id | string | Optional. 1 to 256 characters with no control characters. Unique within the ad account. null clears it. |
fixed_cpm_micros | integer or null | The price per 1,000 viewed impressions, from 100000 ($0.10) to 1000000000 ($1,000), in multiples of 1000. Send this or fixed_cpc_micros on create. See Price. |
fixed_cpc_micros | integer or null | The price per click, from 10000 ($0.01) to 10000000 ($10), in multiples of 10000 (whole cents). Send this or fixed_cpm_micros on create. See Price. |
targeting.surfaces | string[] | One or more of ai, static, ugc, with no repeats. Defaults to all three when omitted on create. Returned in the order ai, static, ugc. |
context_hints | string[] | Optional. Up to 50 phrases describing what the advertiser offers, like "burst pipe". Each is trimmed, then 1 to 100 characters with no control characters. Repeats are dropped, ignoring case, keeping the first. null clears the list. |
status | string | Read-only: active, paused, archived. Change it with the status endpoints. |
effective_status | string | Read-only: active, inactive, archived. See Serving status. |
serving_issues | object[] | Read-only. Why the ad set is not serving: { code, message, source: { object, id } }. Empty when effective_status is active. |
created_time, updated_time | integer | Read-only. Unix seconds. updated_time also moves when effective_status or serving_issues change. |
targeting takes only surfaces. Set geo targets with
/ad_sets/{id}/geo_targets. Any other field, in targeting or
at the top level, is a 400 invalid_value (unknown parameter). A read-only
field in a request body is a 400 invalid_value (<field> is read-only; for
status, status is read-only; use /activate, /pause or /archive).
Price
An ad set has one price, for every ad in it:
| Field | You pay for |
|---|---|
fixed_cpm_micros | Every 1,000 viewed impressions. Clicks are free. |
fixed_cpc_micros | Every valid click. Impressions are free. |
Send exactly one of them on create. The other one is null on the ad set
object. Sending both in one request is a 400 invalid_value, and so is
sending neither on create.
To switch an ad set from one to the other, update it with the other field. For
example, {"fixed_cpc_micros": 1250000} on a CPM ad set makes it $1.25 per
click, and fixed_cpm_micros becomes null. Invalid clicks are never charged.
See Impressions and clicks.
Surfaces
| Value | Where the ad appears |
|---|---|
ai | Inside assistant answers on AI platforms |
static | On publisher pages |
ugc | Next to posts in community feeds |
Create an ad set
POST /ad_sets
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_KEY |
Content-Type | Yes | application/json |
Idempotency-Key | No | 1 to 255 visible characters. See Idempotency. |
The body takes the fields in the table above marked “Required on create” or
“Optional”, and one price: fixed_cpm_micros or fixed_cpc_micros.
curl -X POST https://api.adzen.ai/v1/ad-manager/ad_sets \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 31e2-line-77' \
-d '{
"campaign_id": "cmpn_412",
"external_id": "line-77",
"name": "Plumbing, Dallas-Fort Worth",
"fixed_cpm_micros": 30000000,
"targeting": {
"surfaces": ["ai", "static", "ugc"]
},
"context_hints": ["water heater repair", "burst pipe"]
}'The response is 200 with the new ad set object.
To price the ad set per click instead, send "fixed_cpc_micros": 1250000
($1.25 per click) in place of fixed_cpm_micros.
Ad sets are created active. An ad set serves when its campaign and an
approved ad in it are active. Campaigns are created paused, so a new ad set
spends nothing until you activate its campaign.
| Status | code | When | Retry? |
|---|---|---|---|
404 | not_found | No such campaign in your organization; param is campaign_id | No |
409 | archived | The campaign is archived; param is campaign_id | No |
409 | ad_account_archived | The campaign’s ad account is archived | No |
409 | external_id_conflict | The external_id is already used in this ad account | No |
Retrieve, list and update
| Method | Path | Notes |
|---|---|---|
GET | /ad_sets | List; filter with campaign_id, external_id, status |
GET | /ad_sets/{id} | |
POST | /ad_sets/{id} | Update name, external_id, fixed_cpm_micros or fixed_cpc_micros, targeting, context_hints |
On GET /ad_sets, status must be active, paused or archived. A
malformed campaign_id, like abc, returns an empty list. GET /ad_sets/{id}
and POST /ad_sets/{id} return 200 with the ad set object.
An update changes only the fields you send. An empty body, {}, changes
nothing and returns the ad set as it is. Sending campaign_id is a 400
invalid_value (campaign_id cannot change).
A price or surface change, including a switch between fixed_cpm_micros and
fixed_cpc_micros, applies to every ad in the set in the same call, from
the moment it is saved. If an update returns 503 upstream_unavailable, send
the same request again. Repeating it is safe.
Change status
| Method | Path |
|---|---|
POST | /ad_sets/{id}/activate |
POST | /ad_sets/{id}/pause |
POST | /ad_sets/{id}/archive |
Each returns 200 with the ad set object.
- Pausing or archiving an ad set stops its ads serving.
- Archive is final.
activateorpauseon an archived ad set is a409archived.archiveon an archived ad set returns200. activateon an ad set in an archived ad account is a409ad_account_archived.activateon an ad set that is already active changes none of its ads. Ads you paused one by one stay paused.- After a
503upstream_unavailable, send the same request again. Repeating a status change is safe.
Geo targets
Geo targets are the US locations an ad set targets. They apply to every ad in the set, including ads added later. An ad set with no geo targets targets the whole US.
| Method | Path | Does |
|---|---|---|
GET | /ad_sets/{id}/geo_targets | Read the targets |
POST | /ad_sets/{id}/geo_targets | Replace all of them |
PATCH | /ad_sets/{id}/geo_targets | Add or remove entries |
The geo targets object
{
"object": "geo_targets",
"ad_set_id": "adset_8271",
"include": {
"zipcodes": ["75201", "75204"],
"cities": [{ "city": "FORT WORTH", "state": "TX" }]
},
"exclude": {
"zipcodes": ["75219"],
"cities": []
},
"version": 3,
"updated_time": 1792611600
}| Field | Type | Rules |
|---|---|---|
ad_set_id | string | Read-only. The ad set’s id. |
include | object | Locations to target. Empty or omitted means the whole US. |
exclude | object | Locations to leave out. An exclusion wins over an inclusion. |
include.zipcodes, exclude.zipcodes | string[] | 5-digit US ZIP codes in service, like "75201". Surrounding spaces are trimmed. |
include.cities, exclude.cities | object[] | { "city": "Fort Worth", "state": "TX" }. See Locations. |
version | integer | Read-only. Goes up by one with every change; 0 before the first. Also sent as the ETag header. |
updated_time | integer | Read-only. Unix seconds of the last change; null before the first. |
include and exclude take up to 10,000 entries each, ZIP codes and cities
together. A request body is limited to 2 MiB (2,097,152 bytes).
An ad set whose targets were never set returns version 0:
{
"object": "geo_targets",
"ad_set_id": "adset_8271",
"include": { "zipcodes": [], "cities": [] },
"exclude": { "zipcodes": [], "cities": [] },
"version": 0,
"updated_time": null
}Locations
| Entry | Rule |
|---|---|
| ZIP code | Exactly 5 digits, and a ZIP code in service in the US. |
state | A 2-letter USPS code, in any case: the 50 states, DC, PR, VI, GU, AS or MP. |
city | A US city in that state. Case and punctuation don’t matter. St, Ste, Mt, Ft, Pt, N, S, E and W match Saint, Sainte, Mount, Fort, Port, North, South, East and West. |
Cities are stored and returned in upper case, in Adzen’s reference spelling:
{ "city": "St. Louis", "state": "MO" } comes back as
{ "city": "SAINT LOUIS", "state": "MO" }. Compare cities case-insensitively.
St. Louis and Saint Louis in one list are the same city, so the second is a
duplicate.
Replace the targets
POST /ad_sets/{id}/geo_targets
The body is the complete set. Anything not in it is removed.
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_KEY |
Content-Type | Yes | application/json |
If-Match | No | The ETag you read, like "v3". See Concurrent changes. |
Idempotency-Key | No | POST only. See Idempotency. |
curl -X POST https://api.adzen.ai/v1/ad-manager/ad_sets/adset_8271/geo_targets \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"include": {
"zipcodes": ["75201", "75204"],
"cities": [{ "city": "Fort Worth", "state": "TX" }]
},
"exclude": {
"zipcodes": ["75219"]
}
}'The response is 200 with the geo targets object after the change, plus
status, accepted and rejected. The ETag header carries the new version.
{
"object": "geo_targets",
"ad_set_id": "adset_8271",
"include": {
"zipcodes": ["75201", "75204"],
"cities": [{ "city": "FORT WORTH", "state": "TX" }]
},
"exclude": { "zipcodes": ["75219"], "cities": [] },
"version": 4,
"updated_time": 1792612800,
"status": "applied",
"accepted": {
"include": { "zipcodes": 2, "cities": 1 },
"exclude": { "zipcodes": 1, "cities": 0 }
},
"rejected": []
}| Field | Meaning |
|---|---|
status | applied: the change is complete for the ad set and every ad in it. |
accepted | How many ZIP codes and cities each list now holds. |
rejected | Entries repeated in one list, kept once. See Validation. |
Every POST saves a new version, even when the targets are the same as before.
To clear every target and target the whole US again, send
{"include": {}, "exclude": {}}.
Add or remove entries
PATCH /ad_sets/{id}/geo_targets
Changes part of a long list without sending all of it. Headers are the same as
for POST, without Idempotency-Key.
| Field | Type | Required | Rules |
|---|---|---|---|
add | object | One of add or remove | include and exclude, each with zipcodes and cities, in the same shape as the object. Entries to add. |
remove | object | One of add or remove | Same shape as add. Entries to remove. |
curl -X PATCH https://api.adzen.ai/v1/ad-manager/ad_sets/adset_8271/geo_targets \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"add": { "exclude": { "zipcodes": ["75220"] } },
"remove": { "include": { "zipcodes": ["75204"] } }
}'The response is 200 with the whole geo targets object after the change, with
status, accepted and rejected as for POST, and the new version in the
ETag header:
{
"object": "geo_targets",
"ad_set_id": "adset_8271",
"include": {
"zipcodes": ["75201"],
"cities": [{ "city": "FORT WORTH", "state": "TX" }]
},
"exclude": { "zipcodes": ["75219", "75220"], "cities": [] },
"version": 5,
"updated_time": 1792613400,
"status": "applied",
"accepted": { "added": 1, "removed": 1, "noop": 0 },
"rejected": []
}- Adding an entry that is already there, or removing one that isn’t, counts as
a
noop, not an error. A retriedPATCHchanges nothing the second time: when every entry is anoopon targets that were set before,versionandupdated_timestay as they were. - A
PATCHwith no entries underaddorremoveis a400invalid_value. - The same entry in
addandremoveof one list is a400add_remove_overlap. - The 10,000-entry limit applies to each list after the change.
Validation
Adzen checks every ZIP code and city, in every list, against US reference data
before anything is saved. If any entry is not a US location, the whole request
is refused with 400 invalid_location and the stored targets don’t change.
message lists every bad entry by its position, with a suggestion when a close
match exists, and param is the first one’s position:
{
"error": {
"type": "invalid_request_error",
"code": "invalid_location",
"message": "include.zipcodes[1]: '00000' is not a ZIP code in service in the US; include.cities[0]: 'Atlantis, NY' is not a known US city; did you mean 'ATLANTA, NY'?",
"param": "include.zipcodes[1]"
}
}| Entry | message |
|---|---|
| ZIP code not 5 digits | '7520' is not a 5-digit US ZIP code |
| ZIP code not in service | '00000' is not a ZIP code in service in the US |
| Unknown state | 'TQ' is not a valid 2-letter USPS state code |
| Unknown city | 'Atlantis, NY' is not a known US city, then ; did you mean 'ATLANTA, NY'? when a close match exists |
A city sent as a string, like "Austin, TX", is a 400 invalid_value with
param: "include.cities[0]": send { "city": "Austin", "state": "TX" }.
An entry repeated in one list is kept once and reported in rejected. The
request still succeeds:
{
"list": "include",
"type": "zip",
"value": "75201",
"reason": "duplicate",
"message": "ZIP '75201' appeared more than once; first occurrence kept"
}Concurrent changes
GET returns the current version, and the ETag header carries it as
"v3". Send it back as If-Match: "v3" on a POST or PATCH to make the
change only if nobody else changed the targets since. If they did, the response
is 409 version_conflict and nothing changes: read the targets again and
retry. If-Match accepts "v3", "3", v3 and 3.
Without If-Match:
- A
POSTreplaces whatever is there. The lastPOSTwins. - A
PATCHapplies its entries to the targets as they stand when it runs, so twoPATCHrequests at the same time both take effect. When many changes reach the same ad set’s targets at once, aPATCHcan return500contentionwith nothing changed. Send it again.
Idempotency-Key works on POST, as on every
Ads API POST. A PATCH without If-Match
is safe to repeat as it is.
When a change takes effect
A 200 means the change is complete for the ad set and every ad in it.
A 502 propagation_failed means the targets are saved but the change is
not complete. Send the same request again without If-Match to
complete it, or GET the targets and send it with the new ETag. The same
request with the old If-Match is a 409 version_conflict.
Geo target errors
Errors use the Ads API error body. Authentication
errors, the non-US 403, 429 rate_limit_exceeded and 503
temporarily_unavailable apply to every endpoint and are listed in
Errors.
| Status | code | When | Retry? |
|---|---|---|---|
400 | invalid_location | A ZIP code or city is not a US location; nothing changed | No; fix the entries in message |
400 | invalid_value | A field has the wrong type or is unknown, or a city is a string (param names the field), or a PATCH has no entries under add or remove (no param) | No |
400 | invalid_json | The body is not JSON, or Content-Type is not application/json | No |
400 | add_remove_overlap | The same entry is in add and remove of one list | No |
400 | invalid_if_match | If-Match is not a version like "v3" or 3 | No |
404 | not_found | No such ad set in your organization | No |
409 | archived | The ad set is archived; its targets cannot change | No |
409 | version_conflict | The targets changed since the If-Match version | Yes, after a new GET |
413 | list_too_large | include or exclude would hold more than 10,000 entries | No; split the change |
413 | payload_too_large | The body is over 2 MiB | No; send it in parts with PATCH |
500 | contention | Too many changes to these targets at once; nothing changed | Yes |
500 | internal | A temporary error. | Yes, with backoff |
502 | propagation_failed | The targets are saved, but the change is not complete | Yes, without If-Match |
503 | upstream_unavailable | The ad set could not be read; nothing changed | Yes |
Error responses
Errors use the Ads API error body. Authentication
errors, the non-US 403, 429 rate_limit_exceeded and 503
temporarily_unavailable apply to every endpoint and are listed in
Errors.
| Status | code | When | Retry? |
|---|---|---|---|
400 | invalid_value | A field failed validation, is unknown or is read-only; param names it | No |
400 | invalid_json | The body is not JSON | No |
404 | not_found | No such ad set, or on create no such campaign, in your organization | No |
409 | archived | The ad set, or on create its campaign, is archived and cannot change | No |
409 | ad_account_archived | The ad account is archived; nothing in it can change | No |
409 | external_id_conflict | The external_id is already used in this ad account | No |
503 | upstream_unavailable | The request did not complete | Yes, send the same request again |