Skip to main content
The engagement SDK is a zero-dependency TypeScript client for POST /v1/track. It runs in browsers (with sendBeacon drain on page hide) and in Node 18+ (global fetch). It also carries the one call that reports what a reader did with an article: see Content interactions.
The SDK is not yet published to npm - your 4D contact provides the package during integration onboarding. Everything it does rides on the public /v1/track endpoint, so you can always start with plain HTTP and adopt the SDK later without changing anything server-side.

Quick start

Events are batched (up to 20 per request, the server cap), auto-flushed every 2s while anything is queued, and drained via navigator.sendBeacon when the page is hidden or unloading. Every event carries a client-generated UUID event_id which the server dedupes on, so retries after network failures can never double-count tag affinity.

Identity

The platform only records engagement for known members. Identity comes from one of:
Events tracked with no identity available are dropped and reported through onError - call identify() as soon as you know who the user is.

Delivery semantics

track() never throws. The only error surface is the onError callback:

Content interactions

One call reports what a reader did with an article, from the page that just rendered it:
That posts to POST /v1/content-interactions with the same publishable ek_live_… key the client already holds, so a page that already loads the SDK adds one line. The article ids come from the content API, and Render a club’s articles has it in place on a working page. It takes no tags, and there is no option to add any: the server reads the article’s tags off the article. If a caller could send them, your developers and your marketers would be choosing tag strings for the same vocabulary, and members’ tag affinity would split in two. Leave userId out and the client identity is used instead, when that identity is a platform user id (identify({ userId })). A member external id does not work here, because this endpoint resolves readers by platform user id only. Only signed-in readers can be reported, so an anonymous reader is invisible to these counts rather than counted anonymously. Passing userId blank says exactly that, which is why a blank one drops instead of falling back to the identity: an empty string is what a template renders once the reader signs out, and the read is not theirs. Reports are not batched or queued: the endpoint takes one per request. A read counts once per member, per article, per day, so a refresh does not inflate the number and a redelivered report is deduped rather than double-counted. A refusal arrives as a 202 naming a reason, not as an error, because each one is a normal state of a page rather than a broken request: content_untagged is the one worth wiring an alert for: an article published with no tags reports zero reads forever, and tagging it fixes reads from that point on.
Content interactions never earn XP, and no setting changes that. A read reported by a browser cannot be trusted to pay out. See the content API overview.

Flushing

Both also wait for interaction reports still in the air.

Config reference

Tags

Tags are trimmed + lowercased client-side (matching server normalisation) and must fit ^[a-z0-9][a-z0-9_.:-]{0,63}$ - max 10 per event. Invalid tags are filtered out; an event whose tags are all invalid is dropped and reported. The tracking key is publishable - it ships in client code like an analytics measurement id. Abuse controls are the key’s per-origin allowlist and per-key rate limiting (issue keys via the API), not key secrecy.