UGC API
POST /v1/ugc/process takes one community post and returns at most one ad to show with it.
POST https://api.adzen.ai/v1/ugc/processAdzen returns an ad only when its campaign is active, within budget, and allowed on your inventory.
Headers
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | Your UGC API key. See Authentication. |
Content-Type | Yes | application/json |
Body
| Field | Type | Required | Rules |
|---|---|---|---|
post_id | string or integer | Yes | Your unique id for the post. Don’t use : in it; Adzen removes that character. A number is returned as a string. An empty id returns 422. |
message | string | Yes | The post text. Adzen trims surrounding whitespace. Empty returns 422. Under 10 characters, or over 10,000 bytes (UTF-8), returns 200 with an empty ads array. |
created_at | string or null | No | When the post was created, as an RFC 3339 UTC timestamp, for example "2026-06-24T14:30:45Z". Omit it or send null when unknown. Any other type returns 422. |
geo_target | object | No | Location for this post. Send the most precise signal you have. |
geo_target.zipcode | string | No | US ZIP code, for example "10001". |
geo_target.state | string | No | ISO 3166-2 code ("US-CA") or bare state code ("CA"). |
geo_target.city | string | No | City name, for example "New York". Send state with it. |
geo_target.dma | string | No | Nielsen DMA code, for example "501". |
geo_target.lat | number | No | Latitude in decimal degrees. Send with lon. A non-number returns 422. |
geo_target.lon | number | No | Longitude in decimal degrees. Send with lat. A non-number returns 422. |
geo_target.country | string | No | ISO country code. Defaults to "US". |
Adzen ignores body fields it does not recognize.
Example request
cURL
curl -X POST https://api.adzen.ai/v1/ugc/process \
--max-time 30 \
-H 'Content-Type: application/json' \
-H 'X-API-Key: YOUR_API_KEY' \
-d '{
"post_id": "thread-12345",
"message": "Looking for a good plumber in Seattle, our kitchen sink keeps backing up",
"created_at": "2026-10-29T18:04:12Z",
"geo_target": { "zipcode": "98103" }
}'Response
A 200 has one of three bodies.
| Body | Meaning | What to do |
|---|---|---|
{ "no_ad_retry_after_seconds": N } | No ad matched this post. The body has no ads field. | Show no ad. Do not request this post again for N seconds. See No match. |
{ "post_id": "...", "ads": [] } | No ad is available for this post, or your account is disabled. | Show no ad. |
{ "post_id": "...", "ads": [ad] } | One matched ad. | Fill the tracking macros, render the ad, fire the pixel. |
Check for no_ad_retry_after_seconds before you read ads. Code that
expects every 200 to carry an ads array fails on the no-match body.
Example: matched ad
{
"post_id": "thread-12345",
"ads": [
{
"title": "Same-day plumbing repair in Seattle",
"cta": "Get a free quote from a licensed Seattle plumber.",
"click_through_url": "https://api.adzen.ai/dea/ugc/redirect/q7m2kx9wd4tp?tid={SESSION_ID}&experiment={EXPERIMENT}&treatment={TREATMENT}",
"advertiser_name": "Seattle Plumbing Co",
"pricing_model": { "type": "CPC", "bid_amount": 1.25 },
"relevancy_score": 0.962,
"sponsor_logo_url": "https://cdn.example.com/logos/seattle-plumbing.png",
"ttl_seconds": 28800,
"eligibility_ttl": 259200,
"pixel_trackers": [
{
"vendor": "adzen",
"tracker_type": "impression",
"url": "https://api.adzen.ai/dea/ugc/impression?tid={SESSION_ID}&experiment={EXPERIMENT}&treatment={TREATMENT}&..."
}
]
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
post_id | string | Your post_id after normalization: trimmed, : removed, numbers as strings. |
ads | array | Zero or one ad. |
ads[].title | string | The ad’s headline. At most 120 bytes of UTF-8. A longer title is shortened and ends in …. Can be "". |
ads[].cta | string | The ad’s call-to-action text. At most 50 bytes of UTF-8. |
ads[].click_through_url | string | Adzen click URL. Fill the tracking macros, then open it when the viewer clicks. It redirects to the advertiser’s landing page. |
ads[].advertiser_name | string | The advertiser’s brand name. Always non-empty. |
ads[].pricing_model | object | { "type": "CPC", "bid_amount": number }. For information only. |
ads[].relevancy_score | number | Match strength from 0 to 1. Higher is a closer match. |
ads[].sponsor_logo_url | string | Advertiser logo URL. Always present. "" when the ad has no logo: skip the logo slot. |
ads[].ttl_seconds | integer | Seconds you can keep showing this ad on this post. After that, request the post again. The new response can carry the same ad. |
ads[].eligibility_ttl | integer | For information only. Schedule your next request and stop showing the ad by ttl_seconds. |
ads[].pixel_trackers | array | Impression pixels. Fire each one when the ad is shown. |
ads[].pixel_trackers[].vendor | string | "adzen". |
ads[].pixel_trackers[].tracker_type | string | "impression". |
ads[].pixel_trackers[].url | string | Pixel URL. Fill the tracking macros before you fire it. |
Read ttl_seconds from each response. Do not hard-code it.
Ad unit fields
These fields appear only when your account has ad units configured. Each is present only when the ad unit for this ad declares it. An absent field is left out of the JSON, never sent as null.
| Field | Type | Description |
|---|---|---|
ads[].ad_unit | string | The ad unit this ad was built for. |
ads[].headline | string | Prominent headline, for units with a headline slot. Separate from title. |
ads[].body_text | string | Long-form body copy. |
ads[].offer_text | string | Promotional offer text. |
ads[].image_url | string | Creative image URL. |
ads[].image_dimensions | object | { "width": integer, "height": integer } in pixels. Can be absent: then read the size from the image. |
ads[].disclosure | string | The disclosure label the unit uses. |
When your account has ad units, the unit’s configuration sets which ad text fills title and cta, and their maximum lengths.
Tracking macros
click_through_url and every pixel_trackers[].url contain three macros. Replace each one, wherever it appears, before you render the ad. URL-encode each value.
| Macro | Replace with |
|---|---|
{SESSION_ID} | An id for this ad view. Use the same value in the click URL and the pixel of one ad view, and a new value for each new view. Adzen uses it to tie the impression, the click and any conversion together. |
{EXPERIMENT} | The id of the A/B test the viewer is in. Use an empty string when the viewer is in no test. |
{TREATMENT} | The test arm this viewer is in. Use an empty string when the viewer is in no test. |
Rules for the values:
{SESSION_ID}must be non-empty. A pixel fired with an emptytid(tid=) is not recorded.{EXPERIMENT}and{TREATMENT}accept up to 128 characters fromA-Z,a-z,0-9,_,-,.,:and/. Adzen drops a value outside these rules, and the literal valuesundefinedandnull.- An empty
{EXPERIMENT}or{TREATMENT}, or one left as its literal text, carries no test data. The impression or click is still recorded.
Treat the rest of each URL as opaque. Do not change or remove its other parameters.
Firing the pixel
Request each pixel_trackers[].url once each time the ad is shown, with GET (for example as an image) or POST. It returns 204 No Content. The click URL returns a 302 redirect to the advertiser’s landing page. Both URLs are on https://api.adzen.ai/dea/. See Tracking URLs and tag traffic.
Adzen counts one impression per {SESSION_ID} value per device (IP address and user agent) in a 5-minute window. A second pixel with the same value from the same device within 5 minutes of the previous one is not counted, even for a different ad. Give each ad view its own value.
No match
When no ad matches the post, the 200 body is a single field:
{ "no_ad_retry_after_seconds": 86400 }| Field | Type | Description |
|---|---|---|
no_ad_retry_after_seconds | integer | Do not request this post again until this many seconds have passed. |
The value follows these rules:
- A first miss returns
86400(24 hours). - Each further miss within 24 hours of the previous miss adds
86400:172800, then259200, and so on. After 24 hours with no miss, the window starts at86400again. 600(10 minutes) means the next request for the post can return an ad.
Honor the value you receive. Do not use a fixed interval.
Repeat requests
Adzen stores one result for each post_id under your key. A repeat request for the same post_id returns the stored ad, and does not match the post again, while the ad stays active, within budget and allowed on your inventory. A changed message under the same post_id keeps the stored ad. A new post_id gets a new match. Retrying a request is safe.
Errors
| Status | Body | When | Retry? |
|---|---|---|---|
400 | text/plain | The body is not valid JSON. | No. Fix the request. |
401 | text/plain missing X-Api-Key header | No X-API-Key header. | No. Add the key. |
401 | text/plain unknown API key | The key is not a valid UGC API key. | No. Check the key. |
403 | Not JSON | The request came from outside the US. Production accepts requests from US IP addresses only. This applies to the click and pixel URLs too. | No. Call from a US IP address. |
413 | text/plain | The body is larger than 2 MB. | No. Shorten the body. |
415 | text/plain | Content-Type is missing or is not application/json. | No. Set the header. |
422 | text/plain | post_id or message is missing, empty, or the wrong type; created_at is not a string or null; or geo_target or one of its fields has the wrong type. | No. Fix the field. |
5xx | Varies | A temporary error. | Yes, with exponential backoff. |
Retry only 5xx responses and network timeouts. A 200 with empty ads is not an error.
Timing
Set a client timeout of 30 seconds.