POST /v1/ugc/process
Match a contextual ad to a piece of user-generated content.
Request
Endpoint
POST https://api.adzen.ai/v1/ugc/processHeaders
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | Your API key |
Content-Type | Yes | application/json |
X-Forwarded-For | No | Client IP (for geo targeting) |
Body
{
post_id: string // Your unique identifier for the post
message: string // The post text. Must be non-empty.
created_at?: string // When the source post was created, RFC3339 / ISO 8601
// (e.g. "2026-06-24T14:30:45Z"). Omit or send null if
// unknown.
geo_target?: { // Structured geo signal for the requesting user.
zipcode?: string // US ZIP, e.g. "10001"
state?: string // ISO-3166-2 ("US-CA") or bare code ("CA")
city?: string // e.g. "New York" — pair with state to disambiguate
dma?: string // Nielsen DMA code, e.g. "501"
lat?: number // Decimal degrees. Must be sent together with lon.
lon?: number // Decimal degrees. Must be sent together with lat.
country?: string // ISO country code. Defaults to "US" when omitted.
}
}Send any one geo_target signal — an ad targeted at any granularity
containing the user still matches. Precedence, richest to
coarsest: lat+lon → zipcode → city → dma → state. A country-only
value, or a half lat/lon pair, is not a usable signal and is ignored.
Example
cURL
curl -X POST https://api.adzen.ai/v1/ugc/process \
-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",
"geo_target": { "state": "US-WA" }
}'Response
200 OK
{
"post_id": "thread-12345",
"ads": [
{
"title": "Need a plumber? Same-day service in Seattle",
"cta": "Book Now",
"click_through_url": "https://api.adzen.ai/click/abc123",
"advertiser_name": "Seattle Plumbing Co",
"ttl_seconds": 300,
"pixel_trackers": [
{
"vendor": "adzen",
"tracker_type": "impression",
"url": "https://api.adzen.ai/pixel/abc123"
}
]
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
post_id | string | Echo of the request post_id |
ads | array | Matched ads, ordered by relevance. Can be empty. |
ads[].title | string | Short headline for the ad |
ads[].cta | string | Call-to-action button text |
ads[].click_through_url | string | URL to open on ad click. Use it as given. |
ads[].advertiser_name | string | Human-readable advertiser name |
ads[].ttl_seconds | integer | Refresh the match after this many seconds |
ads[].pixel_trackers | array | Third-party pixels to fire when the ad is displayed |
Error responses
| Status | Meaning |
|---|---|
400 | Validation error (missing post_id, empty message, message too long) |
401 | Missing or invalid API key |
502 | Temporary server error — retry with backoff |
No match
When no ad matches the post, the response is still 200 OK, but the body has a
different shape: a single field and no ads array.
{ "no_ad_retry_after_seconds": 86400 }| Field | Type | Description |
|---|---|---|
no_ad_retry_after_seconds | integer | Don’t request this post again until this many seconds have passed |
The window is usually hours, and it grows each time the same post misses again. It can be much shorter, down to a few minutes, when Adzen is still looking for a match for the post. Honor the value you receive rather than a fixed interval.
Check for no_ad_retry_after_seconds before you read ads. Code that
assumes every 200 has an ads array will fail on this body. The
sandbox returns it on about one in five requests so
you can test this path.
You can also receive { "post_id": "...", "ads": [] }. An empty ads array
is not an error either: it means there is no ad to show for this post right
now.