> ## 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 SDK

> A GPT-style browser client for a club's display ads: define slots, fill them, count impressions

The ads SDK is a zero-dependency browser client for [publisher ad delivery](https://docs.fourdos.dev/concepts/ads.md). It requests an ad for each slot on the page, renders the image, reports the impression when the image loads, and links the click through 4D's redirector. Its surface follows Google Publisher Tag, so a developer who has set up GPT will recognise it: `init`, `defineSlot`, `display`, plus `identify` for member targeting.

It is separate from the [engagement SDK](https://docs.fourdos.dev/sdk/engagement-sdk.md): it shares no endpoint and no payload with `/v1/track`, and a page can load either one without the other.

## Install

The package is `@pellartech/4d-ads-sdk`, published as a prerelease to a private GitHub Packages registry. Your 4D contact grants your GitHub account or repository read access during integration onboarding.

Point the `@pellartech` scope at GitHub Packages in an `.npmrc` you do not commit, with a token that has `read:packages` only, read from your secret store:

```ini theme={null}
@pellartech:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
always-auth=true
```

```bash theme={null}
npm install @pellartech/4d-ads-sdk@0.1.0-rc.3
```

In GitHub Actions, give the workflow `packages: read` and pass its `GITHUB_TOKEN` to npm as `NODE_AUTH_TOKEN`.

> [!NOTE]
>
> The registry token is for your build only. Never put it in a page, a bundle or a committed file. Visitors load the SDK from your own site and never touch the registry.

## Quick start: script tag

The package ships a browser build at `dist/ads.js`. Copy it into your site's public assets during your build and serve it from your own origin:

```bash theme={null}
cp node_modules/@pellartech/4d-ads-sdk/dist/ads.js public/vendor/ads.js
```

Then add three script blocks, in this order:

```html theme={null}
<!-- 1. Command-queue stub. Must come first. -->
<script>
  window.fourdos = window.fourdos || {}; window.fourdos.ads = window.fourdos.ads || { q: [] };
  for (const m of ["init","identify","defineSlot","display"])
    window.fourdos.ads[m] = window.fourdos.ads[m] || function(){ window.fourdos.ads.q.push([m, arguments]); };
</script>

<!-- 2. The SDK. Non-blocking. -->
<script async src="/vendor/ads.js"></script>

<!-- 3. Your page's ad configuration. -->
<script>
  fourdos.ads.init({ key: "ek_live_…", endpoint: "https://api.develop.fourdos.dev" });
  fourdos.ads.defineSlot("home_top_banner", [320, 50], "ad-top");
  fourdos.ads.defineSlot("home_mid_mrec", [300, 250], "ad-mid");
  fourdos.ads.display();
</script>

<div id="ad-top"></div>
<div id="ad-mid"></div>
```

The stub is required. `ads.js` loads `async`, so your calls usually run before it has arrived; the stub queues them, and the SDK runs the queue in call order when it loads. Keep `identify` in the stub's list even if you do not use it yet. Loading `ads.js` twice is harmless: the second copy leaves the first in place.

The containers can appear anywhere in the page, including after the script that defines their slots. `display()` looks them up when it fills.

## Quick start: bundler

```ts theme={null}
import { createAdsClient } from "@pellartech/4d-ads-sdk";

const ads = createAdsClient();
ads.init({ key: "ek_live_…", endpoint: "https://api.develop.fourdos.dev" });
ads.defineSlot("home_top_banner", [320, 50], "ad-top");
await ads.display();
```

A client created this way does not touch `window.fourdos`. Create one per page and reuse it.

## The calls

### `init(options)`

```ts theme={null}
fourdos.ads.init({
  key: "ek_live_…",                            // publishable tracking key (not a secret)
  endpoint: "https://api.develop.fourdos.dev", // ads are served from <endpoint>/v1/ads/serve
  identity: { token: identityToken },          // optional, from your server
  onError: (e) => console.warn("[fourdos.ads]", e.code, e.message, e.unitCode)
});
```

`endpoint` must be an absolute `https` URL. Plain `http` is accepted only for a loopback host such as `http://localhost:3000`, for local development. A missing key or a bad endpoint reports `invalid_config` and leaves the SDK uninitialised. An `identity` that is not `{ token }` reports `invalid_identity`, but `init` still succeeds and the viewer is anonymous, so untargeted ads keep filling.

The key's origin allowlist must include your page's origin, and each ad unit's property in the console must be that same origin. Otherwise every serve is refused or comes back empty; see [Key and origins](https://docs.fourdos.dev/concepts/ads.md#key-and-origins).

### `identify(identity)`

```ts theme={null}
fourdos.ads.identify({ token: identityToken }); // a signed-in fan
fourdos.ads.identify(null);                     // signed out: anonymous again
```

The page cannot name the fan itself. Your server mints an identity token for the signed-in fan with its organization API key and hands it to the page; see [Signed-in fans on ad requests](https://docs.fourdos.dev/authentication.md#signed-in-fans-on-ad-requests) for the call. The SDK sends the token unchanged as `identity_token` on each serve request and never reads it. A token expires one hour after it is minted, so mint a new one before its `expires_at` and call `identify` again with it.

Identity only matters for campaigns with member label rules; everything else serves to anonymous viewers too. Call `identify` before `display()` so the first requests carry it. It applies to later requests and does not refresh slots already shown. Calling it with the identity already in force (`null` while anonymous, or the same token) does nothing. A real change drops any response still in flight for the previous viewer, which leaves that slot unfilled. To request a slot again for the new viewer, call `defineSlot` again for its container and then `display()`. A no-fill leaves the container as it was, so empty it yourself if the previous viewer's ad must not stay up.

Anything other than `{ token }` (a non-blank string of up to 4096 characters) or `null` reports `invalid_identity` and leaves the viewer anonymous. That includes the `{ memberExternalId }` and `{ userId }` shapes of earlier releases, which the server no longer accepts as an identity.

The SDK never sends cookies. Whether an impression or click is stored against the member is decided by the server from your club's consent, and nothing in a serve response names the member; see [Member targeting](https://docs.fourdos.dev/concepts/ads.md#member-targeting).

### `defineSlot(unitCode, [width, height], containerId)`

Declares a slot: the ad unit code from the console, its size in pixels, and the `id` of the element to render into. The size must be the ad unit's size in the console. A served ad of another size is refused and reported as `malformed_response`.

Invalid arguments drop that one slot and report `invalid_slot`; other slots are unaffected. Calling `defineSlot` again for the same container replaces the slot and lets the next `display()` request it again, which is how a single-page app swaps the ad in a box that stays on screen.

### `display()`

Requests every defined slot that has not been requested yet, one serve call per slot, and returns a promise that settles when they are all done. Calling it twice does not request a slot twice.

A filled slot gets `<a rel="noopener sponsored" target="_blank"><img></a>`: the link is 4D's click URL and the image is the creative, built without `innerHTML`. A no-fill leaves the container as it was, so an empty container collapses. No-fill is normal and is not reported.

The impression is sent when the image finishes loading, with `navigator.sendBeacon`, or a `keepalive` fetch when a beacon is unavailable or refused. It goes only to your configured `endpoint`.

## Errors

Nothing in the SDK throws; it runs inside your page's scripts, where an exception would break the page. Every problem goes to `onError` with a `code`, a `message`, and the `unitCode` when it concerns one slot:

| Code | When | What happens |
| - | - | - |
| `invalid_config` | `init` got no key, or an endpoint that is not absolute `https` (or loopback `http`) | The SDK stays uninitialised |
| `not_initialized` | `display()` ran before a successful `init` | Nothing is requested |
| `invalid_identity` | `identify` or `init` got an identity other than `{ token }` or `null` | Later requests are anonymous; `init` still initialises |
| `invalid_slot` | `defineSlot` got a bad unit code, size or container id | That slot is dropped |
| `transport` | The serve call failed on the network or answered non-`2xx`, such as `401` for a bad key, `403` for an unlisted origin or `429` over the rate limit | That slot stays empty |
| `malformed_response` | The serve response could not be read, or the ad does not match the slot's unit code and size | That slot stays empty |
| `missing_container` | No element has the slot's container id when the ad arrives | That slot is skipped |

`transport` with `serve responded 403` almost always means the page's origin is not on the key's allowlist.

## Config reference

| Option | Default | Notes |
| - | - | - |
| `key` | - | Publishable `ek_live_…` tracking key. Ships in page source; not a secret. |
| `endpoint` | - | API base URL, absolute `https` (loopback `http` for local development) |
| `identity` | anonymous | `{ token }`, an identity token your server minted |
| `onError` | no-op | Receives every error listed above |
| `fetch` | `globalThis.fetch` | Injectable transport, for tests or a proxy |
| `document` | `globalThis.document` | Injectable document, for tests or an iframe |
| `sendBeacon` | `navigator.sendBeacon` | Injectable beacon, for tests |

The tracking key is **publishable**, like an analytics measurement id. Its controls are the per-origin allowlist and rate limiting, not secrecy ([issue keys via the API](https://docs.fourdos.dev/walkthroughs/trigger-an-event.md)).


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