Native API reference
Field-level reference of the /ads-api-native endpoint — the request URL and its parameters, the count / rank / tmax controls, the two response shapes, every field of the ad and no-fill objects, and which beacons you actually receive.
This is the integration map for anyone calling Epom’s native endpoint directly — a mobile app, a site backend, or a feed service that renders ads with its own components instead of taking markup from a JS tag. It documents the request URL, the parameters the endpoint reads, the two shapes the response can take, every field of the ad object, and what you must fire back so the impression counts.
The endpoint serves native placements only, and it returns structured creative fields (title, images, CTA text, click URL, beacons) rather than HTML. You decide the layout; Epom decides which creative fills it. For everything else — display, video, email — use the ad tags on the placement’s Ad Tags & API card.
Note: the endpoint is unauthenticated by design. The placement
keyin the URL is what identifies the inventory; it is not a secret API key, and there is no signature. This is the same trust model as a JS ad tag.
Request URL
https://<ad-serving-domain>/ads-api-native?key=<placement key>
/ads-api-native is the only path that reaches this endpoint — there are no aliases.
Take the exact URL from the placement itself — Account → Publishers → site → zone → native placement → Ad Tags & API → API. The field there is built for that placement: it carries the right serving domain and key, and it already includes every default parameter configured on the Settings tab (channel, custom parameters, click chain), so it stays correct as you edit them.
Corner case: the
adssegment of the path is fixed for native placements. If your network renames ad-serving paths, that rename applies to the other API placement types but not to this one — a native placement always answers on/ads-api-native.
Parameters
All parameters are query-string parameters on a GET. Only key is always required.
Native-only controls
| Parameter | What it does | Example | Required |
|---|---|---|---|
count | How many ads to return. Meaning depends on rank — see How many ads come back. | 5 | No |
rank | Selection mode: full (default), fast, none. See Ranking modes. | fast | No |
tmax | Time budget for ad selection, in milliseconds. See Capping the response time. Default: no limit. | 120 | No |
Note:
native_bsis a deprecated alias forcount, kept so existing integrations keep working. New callers should sendcount.
Identification
| Parameter | What it does | Example | Required |
|---|---|---|---|
key | Placement unique identifier | 6d7add73ed34ece3f4e927210fbe471f | Yes |
p | Numeric placement id — an alternative to key | 48213 | No |
ch | Channel value passed in the ad tag | sport-section | No |
click | 3rd-party click-tracking URL prepended to the click chain | http://tracker.example.com/click?u= | No |
cp.<name> | Custom parameter, readable by macros and targeting | cp.section=finance | No |
sub1 … sub10 | Partner pass-through values, recognised without the cp. prefix and available as $$SUB1$$ … $$SUB10$$ macros; they travel with the click to the conversion | sub1=affiliate42 | No |
excluded | Banner ids to keep out of this selection. Repeatable. | 9912 | No |
pb | Passback marker for a chained request | 1 | No |
Server-to-server
When your server calls the endpoint rather than the end user’s device, Epom sees your server’s IP and user agent, which would break targeting, capping and fraud checks. Pass the real client’s values instead:
| Parameter | What it does | Example | Required |
|---|---|---|---|
clientIp | IP of the client | 112.12.23.33 | Yes, for S2S |
clientUa | User agent of the client | Android (Linux; U; Android 2.2.1; …) | Yes, for S2S |
clientOs | Operating system of the client | Android | No |
osv | Device operating system version | 3.1.2 | No |
requestUrl | Client’s request URL | http://foo.bar/foo/bar | No |
requestRef | Client’s request referer URL | http://foo.bar/foo/bar | No |
Note: each of these also accepts a legacy alias (
epomIp,epomUa,ua,epomUrl,epomRef,epomOs,epomOsv, andclientIP). Send the canonical name above; the aliases exist only so older integrations keep working.
A caller that sits in front of the viewer, such as a server-side ad insertion stitcher, can send
the viewer’s address and agent as headers instead: X-Device-IP is read after an explicit
clientIp and ahead of X-Forwarded-For, and X-Device-User-Agent replaces the caller’s own
user agent (an explicit clientUa still wins). X-Device-Accept-Language and X-Device-Referer
carry the viewer’s language and referer.
Device, geo and layout
| Parameter | What it does | Example |
|---|---|---|
ifa | Advertising ID — Android AAID or iOS IDFA (OpenRTB device.ifa) | 38400000-8cf0-11bd-b23e-10b96e40000d |
bundle | Application bundle / package id of in-app inventory (OpenRTB app.bundle) | com.example.game |
ifa_type | What kind of id ifa is — a short lower-case word such as rida, tifa, aaid or sessionid. Sent on to DSPs as device.ext.ifa_type, and only while an ifa is present | aaid |
lmt | Limit Ad Tracking: 1 or true when the viewer opted out, 0 or false when not. Sent on to DSPs as device.lmt; Epom does not act on it itself | 1 |
tz | Client’s UTC offset in hours, as a decimal number | 5.5 |
tzn | Client’s IANA time-zone name | Europe/Kyiv |
latitude / longitude | Client’s coordinates | 3.13192 / 101.684059 |
sw / sh | Device screen width / height, px | 1080 / 2340 |
plWidth / plHeight | Placement width / height for dynamic-size placements, px | 300 / 250 |
For bundle, a value that is still an unsubstituted macro token ([BUNDLE], {{bundle}},
${BUNDLE}, %%BUNDLE%%) is read as not sent. For ifa, an all-zero id, a placeholder word
(null, undefined, unknown, none, n/a) or a macro token is not used to recognise the
viewer, who then falls back to the IP and user-agent fingerprint for capping.
Corner case:
tzis a single decimal number, parsed as a float —-1,2and5.5are three separate examples of valid values, not one value. Sending-1;2;5.5fails to parse and the time zone is treated as unknown.ifais not used for targeting on this endpoint directly; it feeds macros, any onward OpenRTB bid request, and the viewer identity that server-side capping counts against (see Frequency capping).
Privacy signals
| Parameter | What it does | Example |
|---|---|---|
gdpr | GDPR-applies flag: 1 applies, 0 does not | 1 |
gdprConsentIab | IAB TCF consent string | CPtq…base64 |
uc | User has given consent: 1 yes, 0 no | 1 |
gpc | Global Privacy Control opt-out flag (also read from the Sec-GPC header) | 1 |
im | Incognito / private browsing: 1 private, 0 normal, absent unknown | 1 |
The consent signals gate ad serving the same way they do for tags — see GDPR and IAB TCF consent.
How many ads come back
count means two different things depending on rank, because the two modes select
differently:
| Mode | What count means | count absent |
|---|---|---|
rank=full, rank=fast | Exactly how many ads to select (ranked) | 1 ad |
rank=none | A cap on the eligible pool, sampled at random | all eligible creatives |
Corner case:
rank=nonewithoutcountreturns the whole eligible pool, which on a busy placement can be far more creatives than you intended to fetch. Always sendcountwithrank=noneunless you genuinely want everything.
A non-positive count (0 or a negative number) is ignored and the mode’s default applies.
Corner case: a non-numeric
countdoes not fall back to the default — it falls back tonative_bs.count=abc&native_bs=5returns five ads.countonly wins overnative_bswhen it actually parses as an integer.
Response shape
The endpoint always answers JSON — one object or an array of them:
| Request | Body |
|---|---|
rank=full (default) and count absent or 1 | A single JSON object |
rank=full with count > 1 | A JSON array |
rank=fast or rank=none — always, even for one ad | A JSON array |
Corner case: parse defensively on the mode you send.
rank=fast&count=1returns[ { … } ], not{ … }— the array is chosen by the mode, not by how many ads came back.
Corner case: the array promise breaks when the request itself cannot be built — a bad
key, a placement that no longer resolves, a request rejected as a bot. In those cases the endpoint has no parsed request to inspect and answers with a bare object even in the array modes. Accept either shape at the top level.
An ad
{
"type": "native",
"version": "1.0",
"beacons": [{ "type": "impression", "url": "http://adserv.com/impression.gif?b=1" }],
"bannerId": 1042,
"placementId": 318,
"customization": { "theme": "dark" },
"title": "Best offer",
"subtitle": "app of the day",
"description": "this is the best application in its category",
"ctaText": "Click to Install",
"advertiser": "Northwind Media",
"rating": 4.9,
"icons": [{ "width": 128, "height": 128, "url": "http://example.com/icon.png" }],
"images": [{ "width": 320, "height": 50, "url": "http://example.com/image.png" }],
"videos": [{ "width": 640, "height": 360, "url": "http://example.com/promo.mp4" }],
"clickUrl": "http://adserv.com/cr?b=1&u=https%3A%2F%2Fplay.google.com%2F…",
"refreshInterval": 30
}
Every field:
| Field | Type | When it appears |
|---|---|---|
type | string | Always. native on an ad, error on a no-fill. |
version | string | Always. Currently always 1.0. |
beacons | array | When the ad has any — see Beacons. |
bannerId | number | Always on an ad. The creative that was selected. |
placementId | number | Always. |
customization | object | When the creative has custom parameters. String→string; macros already resolved. |
title / subtitle / description / ctaText | string | When the creative carries them. Macros already resolved. |
advertiser | string | Either the advertiser’s name or a fixed value Epom sets network-wide — a server-side setting chooses which. Dropped if that source is empty. |
rating | number | Always. 0.0 when the creative has no rating. |
icons / images / videos | array | When the creative carries that asset. |
clickUrl | string | When the creative has a click URL — see the corner case below. |
refreshInterval | number or string | Always — see the corner case below. |
Asset entries in icons, images and videos are all the same shape — width and height
(numbers, 0 when the size is unset) and url:
{ "width": 320, "height": 50, "url": "http://example.com/image.png" }
Corner case: those three are arrays, but each carries at most one entry. A creative has one icon, one image and one video; the array shape does not mean you can be sent several.
Corner case:
refreshIntervalis present on every ad object, and it is not always a number. When the placement has no refresh interval set, it serializes as the string"UNDEFINE", notnulland not0. A strictly-typed client that maps it to an integer will fail to deserialize a perfectly normal response. Treat the field as “number or string”, and read"UNDEFINE"as “no refresh”.
Corner case:
clickUrlis omitted entirely when the creative has no click URL, rather than being sent empty. Whether such a creative can be selected at all depends on the placement: if URL is one of the placement’s required native fields, the creative is excluded from selection instead. See Native placements.
Which of the optional fields are present depends on what the creative carries and on the required fields configured for the placement — see Native placements and Native creatives. Empty strings, empty arrays and empty objects are dropped rather than sent, and key order is not guaranteed.
No ad
There are two no-fill shapes, and the one you get says where the request stopped.
An ordinary no-fill — the placement is fine, nothing matched — carries the same envelope as an
ad, plus a noImpression beacon:
{
"type": "error",
"version": "1.0",
"message": "no ads",
"beacons": [{ "type": "noImpression", "url": "http://adserv.com/noimp.gif?p=318" }],
"placementId": 318,
"bannerId": null,
"customization": null
}
A failed lookup — an unknown key, a placement missing from the ad-server cache, a request
rejected as a bot, or an internal error — carries only the three envelope fields:
{
"type": "error",
"version": "1.0",
"message": "no ads"
}
Corner case: both shapes use
"message": "no ads", so the message does not distinguish them and cannot be used to diagnose a badkey. The presence ofplacementIdis what tells you the placement resolved. An internal server error also produces the short shape with a 200 — a genuine no-fill and a crash are indistinguishable from the response alone.
Corner case: “no ads” is a 200 response with an error object, not an HTTP error. In the array modes an unfilled request is a shorter array — down to
[]. Treattype: "error"and an empty array as the same “nothing to render” outcome. An array can also contain an error object ([{"type":"error",…}]) when the request fails partway, so checktypeon each element rather than assuming every array member is an ad.
Corner case: a
204 No Contentis possible but rare, and it can only happen in the single-object mode — the array modes always emit at least[]. It means the placement id resolved to a deleted placement, or the request failed on the server side. It never means “no ad matched”.
Beacons
An ad’s beacons array carries the URLs you must fire. Firing them is the caller’s job — Epom
cannot see your rendering, so an ad you display without firing its impression beacon is an ad that
was never counted and never billed.
type | Where you receive it | Fire it when |
|---|---|---|
impression | On an ad object | The ad is actually rendered to the user |
noImpression | On a no-fill object only | Nothing to fire — it is Epom’s own no-fill record |
clickUrl already goes through Epom’s click chain (including any click tracker you passed), so
sending the user to it registers the click by itself.
Corner case: a successful ad object carries impression beacons only. There is no
noImpressionbeacon on an ad you decide not to show, so there is no way to tell Epom you dropped it. If you fetch several ads and render fewer — withrank=none, or by re-ranking acount>1 response — simply do not fire the impression beacons for the ones you drop. That is the whole mechanism: an unfired impression beacon is an uncounted ad.
Corner case: the response contract defines a third beacon type,
click, but the ad server never emits one. Do not build a code path that waits for it. Click accounting happens throughclickUrl.
Ranking modes
rank | What it does | DSP / OpenRTB demand |
|---|---|---|
full (default) | Weighted ranking, one full selection pass per returned ad. The most accurate, the most work. | Included — each pass can run a DSP auction |
fast | Same ranking, but eligibility is computed once and count ads are drawn in a single pass. | Falls back to full when the placement has OpenRTB banners (they need the auction) |
none | No ranking — returns the eligible direct creatives so you can rank them yourself. | Excluded |
Choose full when you want Epom’s optimization to decide. Choose fast when you fetch several
ads at once and the per-ad filter passes cost more than the ranking precision is worth. Choose
none only when you re-rank on your side.
Note: an unrecognized
rankvalue is treated asfullrather than rejected. Matching is case-insensitive and trims whitespace, soFASTandfastboth work.
Corner case: when
rank=fastfalls back to full ranking, the response stays an array. The fallback changes how ads are selected, not the shape you parse.
Corner case: with
rank=none, the pool is only shuffled when it is larger thancount. Ask for as many as exist (or more) and you get them in a stable internal order, not a random one — do not rely onrank=noneto randomize a small pool for you.
Capping the response time with tmax
tmax is a budget in milliseconds, counted from the moment the request arrives — so it covers
request parsing (geo and device resolution) as well as ad selection. When the budget is gone, the
endpoint stops selecting and returns what it already has.
GET /ads-api-native?key=<key>&count=5&tmax=120
Asking for five ads and getting three back is a normal response. Nothing is reported as an error,
and no tmax value causes a failure — the previous behaviour (run to completion) is what you get
when tmax is absent, 0, or negative.
Corner case:
tmaxonly bites on a multi-ad request. The default single-ad path (rank=fullwithcountabsent or1) never consults the budget, sotmaxcannot shorten it and cannot make it return nothing. Sendtmaxalongsidecount>1,rank=fastorrank=none, or it does nothing at all.
Corner case: the budget is checked between ad selections, never inside one. If a single selection is slow — typically because it triggers a DSP sub-auction, which runs on that partner’s own timeout — that selection is not cut short, and the request can overrun
tmaxby roughly the length of it. Sizetmaxas “how long am I willing to wait for more ads”, not as a hard ceiling on the HTTP call; keep your own client-side timeout above it.
Corner case: with
rank=nonethe eligible pool is built and sampled before the budget is ever consulted, sotmaxcannot shorten the expensive part of that mode — only the per-ad response building that follows it.
Corner case: in the multi-ad modes, if the budget is already spent before the first selection, you get
[]. A very smalltmaxon a slow path can therefore return nothing at all, where the same request withouttmaxwould have returned an ad.
Corner cases and gotchas
format,callbackandmaxBidsdo nothing here. They appear in the placement’s shared parameter table because the table serves every API placement type; the native endpoint always answers JSON and never wraps the body in a JSONP callback.- The response is not cached. The endpoint sends
Cache-Control: no-cache, no-store, must-revalidate. Each call is a fresh selection with its own capping, limits and frequency accounting. Do not fetch once and reuse the payload across users. - Capping differs by mode.
fullandfastboth apply advertiser-side request capping per selected ad.rank=nonereturns a pool that has not been through advertiser-side capping at all — inventory-side capping and the shared eligibility filters still run once for the request. If exact capping matters more than latency, stay onfull. - A creative appears at most once per response. Each selection excludes what it already
picked, so a
countlarger than the eligible pool returns fewer ads rather than repeats. rank=noneis not billed per candidate. The pool is not logged as one ad request per candidate; accounting happens when you fire an impression beacon.
Related
- Native placements — creating the placement and its required fields
- Native creatives — the fields an advertiser fills
- Ad tags, invocation codes and the API — where the request URL lives in the UI
- How ad selection works — what “ranked” means
- OpenRTB integration reference — the DSP side of the auction