Skip to main content
The ads SDK is a zero-dependency browser client for publisher ad delivery. 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: 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:
In GitHub Actions, give the workflow packages: read and pass its GITHUB_TOKEN to npm as NODE_AUTH_TOKEN.
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:
Then add three script blocks, in this order:
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

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

The calls

init(options)

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.

identify(identity)

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 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.

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: transport with serve responded 403 almost always means the page’s origin is not on the key’s allowlist.

Config reference

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).