> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fourdos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Ads

> Serve a club's own display ads on its website, and count impressions and clicks

4D runs a first-party ad server for a club's website. The club sets up its inventory and campaigns in the console. The website asks for an ad from the browser with a publishable key, renders the image it gets back, and reports the impression and the click through two more calls. The [ads SDK](https://docs.fourdos.dev/sdk/ads-sdk.md) makes all three calls for you; this page explains what happens underneath.

Ads are image creatives on websites. Nothing the page sends is a secret. The only server-side call on this page mints the identity token that names a signed-in fan, with your organization API key. Nothing here can change inventory or campaigns.

## What the console sets up

Club admins set up the inventory in the console. These are console screens, not partner API:

* **Property.** A website, identified by its origin, for example `https://www.example.com`. Only web properties serve ads. An iOS or Android property can be recorded but never fills.
* **Ad unit.** A slot on a property, with a code and a fixed size in pixels. The code is what your page asks for. Codes are lowercase and match `^[a-z0-9][a-z0-9_.-]{0,63}$`, for example `home_top_banner`.
* **Campaign.** A flight with a start date, an optional end date, a priority, the ad units it runs on, and optional country and member label targeting. Only an `active` campaign serves; `draft`, `paused` and `archived` ones do not.
* **Creative.** An image and a click destination, both `https`, with a size. A creative only fills a unit of exactly the same width and height.

Ask the club's admin for the ad unit codes and sizes your pages should request.

## Key and origins

Ad requests use an `ek_live_…` tracking key, the same kind of publishable key `POST /v1/track` uses. A partner admin issues it with the website's origins on its allowlist; see [Trigger an event](https://docs.fourdos.dev/walkthroughs/trigger-an-event.md) for the call. A key used for tracking can also serve ads.

Two origin checks apply to every serve:

1. The request's `Origin` header must be on the key's allowlist, exactly. A missing or unlisted origin answers `403 FORBIDDEN`.
2. The origin must also be the origin of the property the ad unit belongs to. A unit on another property, or an origin that matches no property, answers as a no-fill.

Browsers send `Origin` on a `fetch()` call but not when loading an `<img>` or an `<iframe>`, so an ad tag that points an image or a frame at the serve URL is always refused. Request ads with `fetch()`, which the SDK does.

## Serve an ad

`GET /v1/ads/serve` picks an ad for one ad unit. It takes the tracking key as `key`, the ad unit code as `unit`, and, for a signed-in fan, an `identity_token` (see [Member targeting](#member-targeting)). The parameters and the response schema are in the [API reference](https://docs.fourdos.dev/api-reference/overview.md) under Endpoints.

```bash theme={null}
curl "$API_BASE_URL/v1/ads/serve?key=ek_live_…&unit=home_top_banner" \
  -H "Origin: https://www.example.com"
```

A fill is `200 OK` with an `ad` object: the creative's `asset_url`, size and ids, plus `click_url`, `impression_url` and `token`, which the next two sections use. With nothing to show, it is `200 OK` with:

```json theme={null}
{ "ad": null, "reason": "no_fill" }
```

A no-fill is normal and is never a `404`. An unknown unit code, an archived unit and a unit with no eligible campaign all look the same, so leave the slot empty and move on.

How an ad is picked:

* The campaign is `active`, its flight has started and has not ended, and the creative is active and the same size as the unit.
* The campaign's country rule, if it has one, matches the viewer's country. The country comes from the request's IP address, coarsely, and the address is not stored. When no country can be resolved, campaigns limited by country do not fill.
* The campaign's label rule, if it has one, matches the viewer's labels. See [Member targeting](#member-targeting).
* Among the eligible creatives, the highest campaign priority wins. Creatives tied at that priority take turns.

`click_url` points at 4D's click redirector, not at the advertiser. `token` is signed and ties the impression and the click to this serve. Every response from the ad endpoints carries `Cache-Control: no-store`; do not cache them.

| Status | Code | When |
| - | - | - |
| `401` | `UNAUTHORIZED` | The key is missing, unknown, revoked, or not an `ek_live_` key |
| `403` | `FORBIDDEN` | The `Origin` header is missing or not on the key's allowlist |
| `429` | `RATE_LIMITED` | The key is over its serve rate limit |

An identity token never causes an error. A missing, malformed, expired or other-club token serves the slot as if the viewer were anonymous.

## Count the impression

When the creative's image has loaded, post the serve token to `POST /v1/ads/impression`. A served ad whose image never loaded is not an impression.

```bash theme={null}
curl -X POST "$API_BASE_URL/v1/ads/impression" \
  -d '{"token":"…"}'
```

The body is read as JSON whatever its `Content-Type`, so `navigator.sendBeacon`, which sends `text/plain`, works. The token can also go in the query string as `?t=`. The body is capped at 4096 bytes.

`202 Accepted`:

```json theme={null}
{ "status": "recorded", "id": "…" }
```

Posting the same token again answers `202` with `{ "status": "duplicate" }` and is not counted twice, so retrying a beacon is safe.

This call takes no key and checks no origin: the signed token is the only credential. A token is good for 30 minutes after the serve.

| Status | Code | When |
| - | - | - |
| `422` | `AD_TOKEN_INVALID` | The token is missing, altered or older than 30 minutes |
| `422` | `VALIDATION_ERROR` | The body is over 4096 bytes |
| `404` | `NOT_FOUND` | The ad unit no longer exists |
| `429` | `RATE_LIMITED` | The organization is over its delivery rate limit |

## Clicks

Use the served `click_url` as the link around the image. `GET /v1/ads/click?t=…` records the click and answers `302` to the advertiser's destination, which 4D looks up when the click arrives. The destination is never in the page, the token or the query string.

A repeated click on the same ad still redirects and is counted once. The redirect carries `Referrer-Policy: no-referrer`, so the advertiser does not learn which page the click came from. A click token is good for 24 hours after the serve.

When the click cannot be recorded, there is no redirect: the call answers `422 AD_TOKEN_INVALID` for a bad or expired token, `404 NOT_FOUND` when the creative or its ad unit no longer exists, or `429 RATE_LIMITED`.

## Rate limits

| Call | Limit |
| - | - |
| `GET /v1/ads/serve` | 1200 requests per minute per key |
| `POST /v1/ads/impression` and `GET /v1/ads/click` | 6000 requests per minute per organization, shared by both |
| `POST /v1/ads/identity-tokens` | 600 requests per minute per organization API key |

A serve over its limit is checked after the key and the origin, so traffic from origins you have not listed cannot use up your allowance. Each ad unit on a page is its own serve request, so a page with three slots makes three.

## Member targeting

A campaign can include or exclude member labels. A viewer must hold at least one included label, when the campaign lists any, and no excluded label. Exclusions win.

To target by label, the serve call has to say who the viewer is, and the page cannot be trusted to say it. Your server mints an identity token for the signed-in fan with its organization API key, `POST /v1/ads/identity-tokens`, and the page passes it as `identity_token` on serve. The call, its body and the one-hour expiry are in [Signed-in fans on ad requests](https://docs.fourdos.dev/authentication.md#signed-in-fans-on-ad-requests). Mint one when the fan signs in and a new one before `expires_at`. The [ads SDK](https://docs.fourdos.dev/sdk/ads-sdk.md) takes it through `identify`.

Send no labels: 4D reads the viewer's current labels from the membership the token names. An anonymous viewer, or a token that names no active member, is only eligible for campaigns with no label rule. Campaigns without label rules serve to everyone, signed in or not. The unsigned `member_external_id` and `user_id` serve parameters of earlier integrations no longer identify anyone, so a request that sends them is served as anonymous.

Targeting uses the member whatever their consent. Recording is different: the impression and the click are stored against the member only when the fan holds your club's **Personalised content** consent at the moment the ad is served. Without it, or without a valid token, they are counted anonymously, as for any viewer.

Nothing the page can read names the member. The identity token is opaque, and the serve response, its token and the click URL carry no readable member id and no labels.

## Next

The [ads SDK](https://docs.fourdos.dev/sdk/ads-sdk.md) wraps all of this in four calls: `init`, `identify`, `defineSlot` and `display`. A creative can click through to a sponsor's draw on your site; your backend reports the fan's entry as described in [Sponsor draw entries](https://docs.fourdos.dev/concepts/ad-entries.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.