---
title: "Native API reference"
description: "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."
type: "reference"
audience: [network-manager, admin]
keywords: [native API, ads-api-native, native ads, count, rank, tmax, native response, beacons, impression beacon, noImpression, server-to-server, S2S, partial result, native feed, refreshInterval, UNDEFINE, videos, clickUrl]
ui_path: "Account → Publishers → site → zone → native placement → Ad Tags & API → API"
---

Fetch the complete documentation index at: https://epom.com/ad-server/docs/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Native API reference

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](https://epom.com/ad-server/docs/reference/placement-types.md) 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](https://epom.com/ad-server/docs/guides/inventory/ad-tags-and-api.md) card.

> **Note:** the endpoint is unauthenticated by design. The placement `key` in 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 `ads` segment 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](#how-many-ads-come-back). | `5` | No |
| `rank` | Selection mode: `full` (default), `fast`, `none`. See [Ranking modes](#ranking-modes). | `fast` | No |
| `tmax` | Time budget for ad selection, in milliseconds. See [Capping the response time](#capping-the-response-time-with-tmax). Default: no limit. | `120` | No |

> **Note:** `native_bs` is a deprecated alias for `count`, kept so existing integrations keep
> working. New callers should send `count`.

### 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`, and `clientIP`). 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:** `tz` is a **single decimal number**, parsed as a float — `-1`, `2` and `5.5`
> are three separate examples of valid values, not one value. Sending `-1;2;5.5` fails to parse
> and the time zone is treated as unknown. `ifa` is 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](https://epom.com/ad-server/docs/guides/delivery/frequency-capping.md)).

### 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](https://epom.com/ad-server/docs/guides/admin/gdpr-tcf-consent.md).

## 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=none` without `count` returns the whole eligible pool, which on a busy
> placement can be far more creatives than you intended to fetch. Always send `count` with
> `rank=none` unless 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** `count` does not fall back to the default — it falls back to
> `native_bs`. `count=abc&native_bs=5` returns five ads. `count` only wins over `native_bs` when
> 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=1` returns
> `[ { … } ]`, 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

```json
{
  "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](#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`:

```json
{ "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:** `refreshInterval` is 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"`**, not `null` and not `0`. 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:** `clickUrl` is **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](https://epom.com/ad-server/docs/guides/inventory/placement-native.md).

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](https://epom.com/ad-server/docs/guides/inventory/placement-native.md) and
[Native creatives](https://epom.com/ad-server/docs/guides/advertising/banner-native.md). 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:

```json
{
  "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:

```json
{
  "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 bad `key`. The presence of `placementId` is 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 `[]`. Treat `type: "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 check `type` on each element rather
> than assuming every array member is an ad.

> **Corner case:** a `204 No Content` is 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
> `noImpression` beacon 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 — with `rank=none`, or by re-ranking a
> `count`>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 through
> `clickUrl`.

## 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 `rank` value is treated as `full` rather than rejected. Matching is
> case-insensitive and trims whitespace, so `FAST` and ` fast ` both work.

> **Corner case:** when `rank=fast` falls 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** than `count`.
> Ask for as many as exist (or more) and you get them in a stable internal order, not a random one
> — do not rely on `rank=none` to 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:** `tmax` only bites on a **multi-ad** request. The default single-ad path
> (`rank=full` with `count` absent or `1`) never consults the budget, so `tmax` cannot shorten it
> and cannot make it return nothing. Send `tmax` alongside `count`>1, `rank=fast` or `rank=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 `tmax` by
> roughly the length of it. Size `tmax` as "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=none` the eligible pool is built and sampled **before** the budget is
> ever consulted, so `tmax` cannot 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 small `tmax` on a slow path can therefore return nothing at all,
> where the same request without `tmax` would have returned an ad.

## Corner cases and gotchas

- **`format`, `callback` and `maxBids` do 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.** `full` and `fast` both apply advertiser-side request capping per
  selected ad. `rank=none` returns 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 on `full`.
- **A creative appears at most once per response.** Each selection excludes what it already
  picked, so a `count` larger than the eligible pool returns fewer ads rather than repeats.
- **`rank=none` is 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](https://epom.com/ad-server/docs/guides/inventory/placement-native.md) — creating the placement and its required fields
- [Native creatives](https://epom.com/ad-server/docs/guides/advertising/banner-native.md) — the fields an advertiser fills
- [Ad tags, invocation codes and the API](https://epom.com/ad-server/docs/guides/inventory/ad-tags-and-api.md) — where the request URL lives in the UI
- [How ad selection works](https://epom.com/ad-server/docs/concepts/how-ad-selection-works.md) — what "ranked" means
- [OpenRTB integration reference](https://epom.com/ad-server/docs/reference/openrtb-integration-reference.md) — the DSP side of the auction