Ads API
The Ads API creates and runs ad accounts, campaigns, ad sets and ads for the advertisers you represent.
Adzen places the ads in AI answers, on publisher pages and in community feeds, next to content that matches what the advertiser offers.
Object model
Organization organization key
└── Ad account one per advertiser, advertiser keys
└── Campaign flight dates, budgets
└── Ad set price, surfaces
└── Ad creative, landing page| Object | What it holds |
|---|---|
| Organization | Your company account. The organization key reaches every ad account in it. |
| Ad account | One advertiser, its spend cap and its advertiser keys. |
| Campaign | When it runs, the daily and lifetime budgets, and the parameters on every landing URL. |
| Ad set | The price and surfaces. Every ad in the set inherits them. Geo targets are set with their own endpoint. |
| Ad | The creative and its landing page. |
Base URL
https://api.adzen.ai/v1/ad-managerProduction accepts requests from US IP addresses. Requests from other
addresses get 403.
Authentication
Send your key as a bearer token:
curl https://api.adzen.ai/v1/ad-manager/organization \
-H 'Authorization: Bearer YOUR_API_KEY'Use the bearer form. The X-API-Key: YOUR_API_KEY header also works, but a
rejected key then gets a different error body (see Errors). If a
request carries both, X-API-Key is used.
There are two kinds of key:
| Key | Reaches | Issued by |
|---|---|---|
| Organization key | Every ad account in your organization | Adzen. After a rotation the old key works for 14 days. See Rotating keys. |
| Advertiser key | One ad account | You, with the organization key. See Advertiser API keys. |
For key types and rotation, see Authentication.
Getting started
Build an advertiser in this order. Each step returns the new object with the state shown.
| Step | Call | State after the call |
|---|---|---|
| 1 | POST /ad_accounts | status: active, review_status: pending. Adzen reviews the account. |
| 2 | POST /campaigns | status: paused |
| 3 | POST /ad_sets | status: active |
| 4 | POST /files | The image is stored. Optional: an ad can run without one. |
| 5 | POST /ads | status: paused, review_status: in_review. Adzen reviews the ad. |
| 6 | POST /ads/{id}/activate | status: active |
| 7 | POST /campaigns/{id}/activate | status: active |
You can run every step while the account and the ad are in review. The ad
serves once Adzen approves the ad account and the ad
(review_status: approved on both). Each campaign, ad set and ad reports whether it can serve
in effective_status, and why not in serving_issues. See
Serving status.
Conventions
| Topic | Rule |
|---|---|
| IDs | A type prefix and a positive integer: org_12, acct_61, cmpn_412, adset_8271, ad_2864, file_12, key_93. Responses always use this form. On input the bare number (412) is accepted too. An id with another type’s prefix is refused. |
| Your own ids | Every ad account, campaign, ad set and ad takes an optional external_id. Look objects up with ?external_id=. |
| Money | Integer micros of USD. $2.50 is 2500000. |
| Timestamps | Unix seconds. |
| Days | Insights, statements and invoices use your organization’s time zone. A campaign’s daily budget uses the zone the campaign was created with: see Budgets. |
| Updates | POST /{resource}/{id} with only the fields you are changing. |
| Status changes | POST /{resource}/{id}/activate, /pause or /archive. See Status changes. |
Request bodies
- Create and update bodies are a JSON object, sent with
Content-Type: application/json(or anotherapplication/*+jsontype). A body that is not JSON, is empty, or has no JSONContent-Typeis400invalid_json. JSON that is not an object is400invalid_valuewithparambody. - A field the endpoint does not take, or a read-only field (
id,object,status,effective_status,serving_issues,created_time,updated_time), is400invalid_value.paramnames the field. nullclears an optional field that has no default. To returnattribution_window_daysto its default, send30.- An update with
{}changes nothing and returns the object as it is. activate,pauseandarchivetake no body.- JSON bodies are limited to 2 MiB. A larger body is
413payload_too_large. - Validation stops at the first invalid field, and
paramnames it. Geo targets are the exception: every invalid location is listed in one response. - A successful create returns
200with the new object.
Status changes
activate,pauseandarchivereturn200with the updated object.- Repeating the current status is a no-op and returns
200. - Archive is final. Archiving an archived object returns
200. Activating or pausing an archived object is409archived.
Lists
Every list endpoint returns the same envelope:
{
"object": "list",
"data": [{ "id": "cmpn_412", "object": "campaign" }],
"first_id": "cmpn_412",
"last_id": "cmpn_412",
"has_more": false
}Every list takes these parameters:
| Parameter | Description |
|---|---|
limit | 1 to 500, default 100 |
after | An id of the listed type. Returns the page after it. |
before | An id of the listed type. Returns the page before it. Sending both after and before is 400 (param is before). |
order | desc (default, highest id first) or asc |
Some lists take more:
| Parameter | Lists | Description |
|---|---|---|
external_id | /ad_accounts, /campaigns, /ad_sets, /ads | Exact match on your own id |
updated_after | Every list except /ad_accounts/{id}/api_keys | Unix seconds. Returns objects with updated_time at or after this time. On /files it matches created_time. See Keeping in sync. |
Results are ordered by id, not by updated_time. Page with after set to the
last_id of the previous page while has_more is true. A query parameter a
list does not take is ignored.
Idempotency
Send an Idempotency-Key header on every POST, including POST /files. A
retry with the same key and the same request within 24 hours gets the result
shown in the table below.
- The key is 1 to 255 visible characters. Anything else is
400invalid_idempotency_key. - Only
POSTrequests use it. - Keys are scoped to your API key.
- “The same request” means the same method, path,
If-Matchheader and body. JSON bodies are compared as JSON, so field order does not matter. - A request runs to the end even if your connection drops. Retry with the same key to get its response.
| First request’s outcome | A retry with the same key and request |
|---|---|
2xx, 400, 413, 415 or 422 | Gets the stored response with Idempotent-Replayed: true. Nothing runs again. |
Any other status (for example 404, 409, 429 or 5xx) | Runs again |
| Still running | 409 idempotency_key_in_use with Retry-After: 1 |
| Created an API key | 409 secret_shown_once. See Create a key. |
The same key with a different request is 409 idempotency_key_reused. A
request you correct after a 400 is a different request: send it with a new
key.
A POST can get 503 temporarily_unavailable with Retry-After: 5. Nothing
ran. Retry with the same key.
Errors
{
"error": {
"type": "invalid_request_error",
"code": "invalid_value",
"message": "fixed_cpm_micros must be a multiple of 1000 from 100000 ($0.10) to 1000000000 ($1,000)",
"param": "fixed_cpm_micros"
}
}param is null when no single field is at fault. Branch on type and
code. message is text for people; do not parse it.
When the key is sent in X-API-Key, a rejection of the key itself (401, and
403 or 503 from the key check) has the body
{"error": "...", "message": "..."} instead. Bearer callers always get the
envelope above.
| Status | type | code | When | Retry? |
|---|---|---|---|---|
400 | invalid_request_error | invalid_value | A field or query parameter is out of range or of the wrong type, or a body field is unknown or read-only. param names it, like include.cities[0]. | No. Fix the request. |
400 | invalid_request_error | invalid_json | The body is not JSON, is empty, or has no JSON Content-Type. | No |
400 | invalid_request_error | invalid_query | The query string cannot be read. | No |
400 | invalid_request_error | invalid_idempotency_key | Idempotency-Key is not 1 to 255 visible characters. | No |
400 | invalid_request_error | invalid_image | An uploaded file is empty, over 25 MB, or not a PNG or JPEG. See Images. | No |
400 | invalid_request_error | too_many_rows | An insights response would hold more than 20,000 rows. | No. Narrow the request. |
400 | invalid_request_error | scope_too_large | An insights level holds more than 20,000 campaigns or ads. | No. Ask per ad account or per campaign. |
400 | invalid_request_error | invalid_location | A geo target location is not recognized. See Geo targets. | No |
401 | authentication_error | missing_api_key | No key was sent. | No |
401 | authentication_error | invalid_api_key | The key is unknown, revoked or expired, or it is a key for another Adzen product (UGC API, AI API). | No |
403 | permission_error | account_disabled | The key’s organization or ad account is disabled. | No |
403 | permission_error | organization_key_required | An advertiser key called an endpoint that takes only the organization key. | No |
403 | none | none | The request came from an IP address outside the US. The body is not the error envelope. | No |
404 | not_found_error | not_found | No such object in your organization, or no such path. When the id came from your request body, param names the field, like campaign_id or creative.file_id. | No |
405 | invalid_request_error | method_not_allowed | The path does not take this method. | No |
409 | conflict_error | external_id_conflict | Another object already uses this external_id. param is external_id. | No |
409 | conflict_error | archived | The object, or the parent you are adding to, is archived. | No |
409 | conflict_error | ad_account_archived | The ad account is archived. Nothing in it can be created or changed. | No |
409 | conflict_error | flight_ended | You activated a campaign whose end_time has passed. Set a later end_time first. | No |
409 | conflict_error | ad_busy | Another change to the same ad has not finished. | Yes, with backoff |
409 | conflict_error | managed_by_adzen | Adzen manages this object; it can be read but not changed. | No |
409 | conflict_error | organization_inactive | Your organization is not active. | No |
409 | conflict_error | ad_account_inactive | The ad account is not active and cannot get new keys. | No |
409 | conflict_error | key_limit_reached | The ad account already has 20 active keys. | No |
409 | conflict_error | idempotency_key_reused | The Idempotency-Key was used with a different request. | No. Use a new key. |
409 | conflict_error | idempotency_key_in_use | The first request with this key is still running. | Yes, after Retry-After (1 second) |
409 | conflict_error | secret_shown_once | A retry of an API key create that already succeeded. | No |
413 | invalid_request_error | payload_too_large | The body is over the size limit (2 MiB for JSON). | No |
429 | rate_limit_error | rate_limit_exceeded | Over a rate limit. | Yes, after Retry-After |
500 | api_error | internal_error | A temporary error. | Yes, with backoff |
503 | api_error | authentication_unavailable | The service is briefly unavailable. The key was not rejected: keep using it. | Yes, after Retry-After (1 second) when set |
503 | api_error | temporarily_unavailable | Writes are briefly unavailable. Nothing ran. | Yes, after Retry-After (5 seconds) |
502 or 503 | api_error | upstream_unavailable | A temporary error. Send the same request again, with the same Idempotency-Key on a POST. | Yes, with backoff |
Other 5xx | api_error when the body is the envelope | varies | A server or network error. | Yes, with exponential backoff, after Retry-After when set |
Geo targets have codes of their own. See Geo target errors.
Request IDs
Every response from the API carries a Request-Id header. A rejection of the
key itself (401, and 403 or 503 from the key check) and the non-US
403 can come without one. Adzen generates ids as req_ and 32 hex
characters. You can send your own Request-Id (or X-Request-Id), 1 to 128
characters of letters, digits, -, _, . or :, and the response echoes
it. Quote the id when you contact Adzen about a request.
Rate limits
| Limit | Requests per minute |
|---|---|
| Per endpoint, per key | 600 |
| Per key, all endpoints | 1,200 |
Hourly insights at ad set or ad level, or broken down by ad_set or ad, per key | 30 |
- An endpoint is the method plus the route template. Pausing two campaigns
counts twice against
POST /campaigns/{id}/pause. - Limits count per API key. Each advertiser key has its own.
- Each window is one clock minute and starts at second 0.
- Requests answered
429and idempotent replays count too.
Over a limit, the API returns 429 rate_limit_exceeded with Retry-After,
the seconds until the next minute starts.
The headers on every response to an authenticated request describe the limit with the fewest requests left:
| Header | Value |
|---|---|
X-RateLimit-Limit | Requests allowed in the current minute |
X-RateLimit-Remaining | Requests left in it |
X-RateLimit-Reset | Unix seconds when it resets |
See Rate limits for every product’s limits.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /organization | Check the key |
GET | /organization/insights | Metrics for the whole organization |
GET POST | /ad_accounts | List; create |
GET POST | /ad_accounts/{id} | Retrieve; update |
POST | /ad_accounts/{id}/activate, /pause, /archive | Change status |
GET POST | /ad_accounts/{id}/api_keys | List; create advertiser keys |
POST | /ad_accounts/{id}/api_keys/{key_id}/revoke | Revoke an advertiser key |
GET POST | /campaigns | List; create |
GET POST | /campaigns/{id} | Retrieve; update |
POST | /campaigns/{id}/activate, /pause, /archive | Change status |
GET POST | /ad_sets | List; create |
GET POST | /ad_sets/{id} | Retrieve; update |
POST | /ad_sets/{id}/activate, /pause, /archive | Change status |
GET POST PATCH | /ad_sets/{id}/geo_targets | Read, replace or edit geo targets |
GET POST | /ads | List; create |
GET POST | /ads/{id} | Retrieve; update |
POST | /ads/{id}/activate, /pause, /archive | Change status |
GET POST | /files | List; upload an image |
GET | /files/{id} | Retrieve an image |
POST | /conversions | Send conversions |
GET | /{object}/{id}/insights | Metrics for an ad account, campaign, ad set or ad |