Skip to Content

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 }
FieldTypeRules
campaign_idstringRequired on create. cmpn_412 or 412. Cannot change.
namestringRequired on create. Trimmed, then 3 to 256 characters with no control characters.
external_idstringOptional. 1 to 256 characters with no control characters. Unique within the ad account. null clears it.
fixed_cpm_microsinteger or nullThe 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_microsinteger or nullThe 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.surfacesstring[]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_hintsstring[]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.
statusstringRead-only: active, paused, archived. Change it with the status endpoints.
effective_statusstringRead-only: active, inactive, archived. See Serving status.
serving_issuesobject[]Read-only. Why the ad set is not serving: { code, message, source: { object, id } }. Empty when effective_status is active.
created_time, updated_timeintegerRead-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:

FieldYou pay for
fixed_cpm_microsEvery 1,000 viewed impressions. Clicks are free.
fixed_cpc_microsEvery 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

ValueWhere the ad appears
aiInside assistant answers on AI platforms
staticOn publisher pages
ugcNext to posts in community feeds

Create an ad set

POST /ad_sets

HeaderRequiredValue
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json
Idempotency-KeyNo1 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.

StatuscodeWhenRetry?
404not_foundNo such campaign in your organization; param is campaign_idNo
409archivedThe campaign is archived; param is campaign_idNo
409ad_account_archivedThe campaign’s ad account is archivedNo
409external_id_conflictThe external_id is already used in this ad accountNo

Retrieve, list and update

MethodPathNotes
GET/ad_setsList; 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

MethodPath
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. activate or pause on an archived ad set is a 409 archived. archive on an archived ad set returns 200.
  • activate on an ad set in an archived ad account is a 409 ad_account_archived.
  • activate on an ad set that is already active changes none of its ads. Ads you paused one by one stay paused.
  • After a 503 upstream_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.

MethodPathDoes
GET/ad_sets/{id}/geo_targetsRead the targets
POST/ad_sets/{id}/geo_targetsReplace all of them
PATCH/ad_sets/{id}/geo_targetsAdd 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 }
FieldTypeRules
ad_set_idstringRead-only. The ad set’s id.
includeobjectLocations to target. Empty or omitted means the whole US.
excludeobjectLocations to leave out. An exclusion wins over an inclusion.
include.zipcodes, exclude.zipcodesstring[]5-digit US ZIP codes in service, like "75201". Surrounding spaces are trimmed.
include.cities, exclude.citiesobject[]{ "city": "Fort Worth", "state": "TX" }. See Locations.
versionintegerRead-only. Goes up by one with every change; 0 before the first. Also sent as the ETag header.
updated_timeintegerRead-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

EntryRule
ZIP codeExactly 5 digits, and a ZIP code in service in the US.
stateA 2-letter USPS code, in any case: the 50 states, DC, PR, VI, GU, AS or MP.
cityA 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.

HeaderRequiredValue
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json
If-MatchNoThe ETag you read, like "v3". See Concurrent changes.
Idempotency-KeyNoPOST 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": [] }
FieldMeaning
statusapplied: the change is complete for the ad set and every ad in it.
acceptedHow many ZIP codes and cities each list now holds.
rejectedEntries 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.

FieldTypeRequiredRules
addobjectOne of add or removeinclude and exclude, each with zipcodes and cities, in the same shape as the object. Entries to add.
removeobjectOne of add or removeSame 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 retried PATCH changes nothing the second time: when every entry is a noop on targets that were set before, version and updated_time stay as they were.
  • A PATCH with no entries under add or remove is a 400 invalid_value.
  • The same entry in add and remove of one list is a 400 add_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]" } }
Entrymessage
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 POST replaces whatever is there. The last POST wins.
  • A PATCH applies its entries to the targets as they stand when it runs, so two PATCH requests at the same time both take effect. When many changes reach the same ad set’s targets at once, a PATCH can return 500 contention with 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.

StatuscodeWhenRetry?
400invalid_locationA ZIP code or city is not a US location; nothing changedNo; fix the entries in message
400invalid_valueA 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
400invalid_jsonThe body is not JSON, or Content-Type is not application/jsonNo
400add_remove_overlapThe same entry is in add and remove of one listNo
400invalid_if_matchIf-Match is not a version like "v3" or 3No
404not_foundNo such ad set in your organizationNo
409archivedThe ad set is archived; its targets cannot changeNo
409version_conflictThe targets changed since the If-Match versionYes, after a new GET
413list_too_largeinclude or exclude would hold more than 10,000 entriesNo; split the change
413payload_too_largeThe body is over 2 MiBNo; send it in parts with PATCH
500contentionToo many changes to these targets at once; nothing changedYes
500internalA temporary error.Yes, with backoff
502propagation_failedThe targets are saved, but the change is not completeYes, without If-Match
503upstream_unavailableThe ad set could not be read; nothing changedYes

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.

StatuscodeWhenRetry?
400invalid_valueA field failed validation, is unknown or is read-only; param names itNo
400invalid_jsonThe body is not JSONNo
404not_foundNo such ad set, or on create no such campaign, in your organizationNo
409archivedThe ad set, or on create its campaign, is archived and cannot changeNo
409ad_account_archivedThe ad account is archived; nothing in it can changeNo
409external_id_conflictThe external_id is already used in this ad accountNo
503upstream_unavailableThe request did not completeYes, send the same request again
Last updated on