Skip to Content

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/process

Adzen returns an ad only when its campaign is active, within budget, and allowed on your inventory.

Headers

HeaderRequiredValue
X-API-KeyYesYour UGC API key. See Authentication.
Content-TypeYesapplication/json

Body

FieldTypeRequiredRules
post_idstring or integerYesYour 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.
messagestringYesThe 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_atstring or nullNoWhen 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_targetobjectNoLocation for this post. Send the most precise signal you have.
geo_target.zipcodestringNoUS ZIP code, for example "10001".
geo_target.statestringNoISO 3166-2 code ("US-CA") or bare state code ("CA").
geo_target.citystringNoCity name, for example "New York". Send state with it.
geo_target.dmastringNoNielsen DMA code, for example "501".
geo_target.latnumberNoLatitude in decimal degrees. Send with lon. A non-number returns 422.
geo_target.lonnumberNoLongitude in decimal degrees. Send with lat. A non-number returns 422.
geo_target.countrystringNoISO country code. Defaults to "US".

Adzen ignores body fields it does not recognize.

Example request

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.

BodyMeaningWhat 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

FieldTypeDescription
post_idstringYour post_id after normalization: trimmed, : removed, numbers as strings.
adsarrayZero or one ad.
ads[].titlestringThe ad’s headline. At most 120 bytes of UTF-8. A longer title is shortened and ends in …. Can be "".
ads[].ctastringThe ad’s call-to-action text. At most 50 bytes of UTF-8.
ads[].click_through_urlstringAdzen click URL. Fill the tracking macros, then open it when the viewer clicks. It redirects to the advertiser’s landing page.
ads[].advertiser_namestringThe advertiser’s brand name. Always non-empty.
ads[].pricing_modelobject{ "type": "CPC", "bid_amount": number }. For information only.
ads[].relevancy_scorenumberMatch strength from 0 to 1. Higher is a closer match.
ads[].sponsor_logo_urlstringAdvertiser logo URL. Always present. "" when the ad has no logo: skip the logo slot.
ads[].ttl_secondsintegerSeconds 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_ttlintegerFor information only. Schedule your next request and stop showing the ad by ttl_seconds.
ads[].pixel_trackersarrayImpression pixels. Fire each one when the ad is shown.
ads[].pixel_trackers[].vendorstring"adzen".
ads[].pixel_trackers[].tracker_typestring"impression".
ads[].pixel_trackers[].urlstringPixel 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.

FieldTypeDescription
ads[].ad_unitstringThe ad unit this ad was built for.
ads[].headlinestringProminent headline, for units with a headline slot. Separate from title.
ads[].body_textstringLong-form body copy.
ads[].offer_textstringPromotional offer text.
ads[].image_urlstringCreative image URL.
ads[].image_dimensionsobject{ "width": integer, "height": integer } in pixels. Can be absent: then read the size from the image.
ads[].disclosurestringThe 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.

MacroReplace 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 empty tid (tid=) is not recorded.
  • {EXPERIMENT} and {TREATMENT} accept up to 128 characters from A-Z, a-z, 0-9, _, -, ., : and /. Adzen drops a value outside these rules, and the literal values undefined and null.
  • 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 }
FieldTypeDescription
no_ad_retry_after_secondsintegerDo 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, then 259200, and so on. After 24 hours with no miss, the window starts at 86400 again.
  • 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

StatusBodyWhenRetry?
400text/plainThe body is not valid JSON.No. Fix the request.
401text/plain missing X-Api-Key headerNo X-API-Key header.No. Add the key.
401text/plain unknown API keyThe key is not a valid UGC API key.No. Check the key.
403Not JSONThe 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.
413text/plainThe body is larger than 2 MB.No. Shorten the body.
415text/plainContent-Type is missing or is not application/json.No. Set the header.
422text/plainpost_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.
5xxVariesA 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.

Last updated on