Skip to main content

What this lets you do

Send pageviews and revenue conversions to Traceten from your Node.js backend, over server-to-server HTTP that survives ad-blockers and privacy browsers. Use it where the browser snippet cannot run: server-rendered or headless flows, background jobs, webhook handlers, and mobile or API backends.

Before you start

  • Node.js 18 or newer.
  • Your Traceten siteId, from Sites → Install in the dashboard.
  • Your ingest host URL, for example https://ingest.traceten.com.
  • A visitorId for each event. The SDK runs on your server, so there is no Traceten cookie to read. You supply the identifier from your own request context. It is either a UUID or an h:<64-hex> identify hash. See supplying identifiers.

Install

npm and yarn work too:
The only runtime dependency is undici.

Minimal example

How it behaves

page() and track() buffer the event and return immediately. The SDK sends events in the background: when the events queue reaches flushAt, every flushInterval milliseconds, on an explicit flush(), and on process exit. These methods never throw because of a network problem. A failed delivery surfaces through your onError hook, never as an exception in your request path. They do throw synchronously on a programmer error, so you catch a mis-shaped call in development instead of a silent server-side rejection:
  • track() called without a visitorId
  • an eventName that fails its pattern
  • a url that is not a valid URL
  • a valueCents that is not a non-negative integer

Configuration

host must be an absolute http(s) URL. A trailing slash is stripped. There is no default host: each deployment has its own ingest URL. host must use https unless it is a loopback address. The API key is sent as a Bearer token on every request, so a plaintext http host would transmit a live credential in cleartext; the SDK refuses one at construction. apiKey is required. See The API key below.

The API key

apiKey is required. Create a key in the dashboard under Settings, API keys, or use the key shown once when you created the site. The key must carry the Send events permission (ingest:write). New keys are created read-only by default, so tick that box when you create one, or the API will reject your calls. The key created automatically with a new site already has it. See permissions. Keep it on your server. It is a secret: never put it in client-side code, a mobile app, or a public repository. It is not the same value as siteId, which is public and already embedded in your pages. The key does two things:
  1. It gets you in. The SDK posts to {host}/v1/server/events, {host}/v1/server/conversions and {host}/v1/server/payments, which return 401 without a valid key. The open /v1/events and /v1/conversions endpoints stay open for the browser snippet, which cannot hold a secret.
  2. It gets you your own quota. Authenticated traffic is rate-limited on a bucket tied to the key, separate from the shared per-site bucket. siteId is public, so anyone who can read your page source can send events under it. With a key, that traffic cannot exhaust your allowance and 429 your conversion calls.
The constructor validates the key’s shape and throws if it is missing or malformed. An integration that silently drops every event is worse than one that fails on its first line. To rotate a key: create the new one, deploy it, then revoke the old one. Revocation normally takes effect at the edge within about a minute. If our database is unreachable at that moment, an edge location that was already using the key may keep honouring it for up to about fifteen minutes more, so that a database blip cannot silently drop your events.

API reference

new Client(options)

page(props)

Enqueues a pageview to POST {host}/v1/server/events.
Which user agent gets recorded. The userAgent you pass is what Traceten stores on the event and classifies on. Omit it and the event falls back to the User-Agent header the SDK’s own HTTP client sent, which describes your server rather than the visitor. That substitution applies only to requests authenticated with an API key. The browser snippet cannot override its own User-Agent this way.

track(name, opts)

Enqueues a conversion to POST {host}/v1/server/conversions. name must match ^[a-z][a-z0-9_]*$ (no hyphen). visitorId is required: a conversion cannot be attributed without it. valueCents is in minor units. For $49.00, pass 4900. The field is spelled out as valueCents (not value) to prevent the dollars-versus-cents mistake.
Keep visitorId opaque: a random first-party id or a salted hash you control. Never a bare sha256(email) or any hash of an email or phone number (that is re-identifiable PII). The SDK enforces the shape (UUID or h:<64-hex>) but cannot see what you hashed. Keep properties to ids and enums such as { plan: "pro" }. Do not put emails, names, or phone numbers in them.

goal(name, opts)

Records a goal completion. Same arguments, same endpoint, and same payload as track(), with one difference: it throws on the reserved names, which belong to the Stripe and Shopify integrations. Reserved: payment, free_trial, trial_started, trial_converted, subscription_started, subscription_upgraded, subscription_downgraded, subscription_renewed, subscription_cancel_scheduled, subscription_reactivated, subscription_ended. track() still accepts them, because that is how those events are legitimately sent. Goal property keys and values are both stored, and both are read back: GET /v1/goals/{name}/properties returns each key and its most common values, and the dashboard renders the same breakdown. Keep them to ids and enums. See what happens to property keys and values.

payment(props)

Records a payment from any payment processor via POST {host}/v1/server/payments. See the Payment API reference for the full contract. Unlike page() and track(), this one sends immediately and returns what the server did with the payment. amount is the major unit, the opposite of track()’s valueCents. For $49.99 pass 49.99, and for ¥5000 pass 5000. It is the number you read off your processor.
Partial refunds do not go through this method. refunded: true takes back everything still outstanding on the transaction, and this SDK has no field for a smaller figure. To refund part of a payment, POST to /v1/server/payments with refunded_amount, which is the total refunded against the transaction so far.
Calling it again with the same transactionId returns "duplicate" and creates nothing. That is why the SDK retries a 5xx here. It rejects when the payment could not be delivered after every retry, and when a field is invalid. It is the only method in this SDK that reports a delivery failure to the caller, because a dropped payment is missing revenue rather than a missing pageview. Wrap it accordingly:
Do not send payments here for a processor you have also connected natively. Traceten would record the payment twice and overstate your revenue. See avoiding duplicate payments.

flush()

Sends everything queued now. Resolves once every in-flight request settles. Never rejects.

close() / shutdown()

Flushes, stops the background timer, and removes the exit hooks. Idempotent. After close(), page(), track() and payment() throw. shutdown() is an alias.

Batching and retries

  • Pageviews are sent to /v1/server/events in batch envelopes of up to 50 events. If you enqueue 120 pageviews, the SDK sends them as batches of 50, 50, and 20.
  • Conversions are sent to /v1/server/conversions one body per event.
  • Payments are sent to /v1/server/payments immediately, one body per call, and are not buffered.
  • On a 5xx, a 429, or a network or timeout error, the batch is retried with exponential backoff and full jitter, up to maxRetries times.
  • On any other 4xx the payload is malformed. The SDK drops it and calls onError. It does not retry, because retrying a rejected payload forever is a self-inflicted denial of service.

Graceful shutdown

By default the SDK registers best-effort flush hooks on beforeExit, SIGTERM, and SIGINT, so a normal shutdown does not lose buffered events. For full control, flush explicitly and disable the hooks:
Data loss on a hard kill (SIGKILL, power loss) is expected. Data loss on a graceful shutdown is not.

Verify it worked

  1. Send one page() call from your backend.
  2. Open Sites → Events in the Traceten dashboard for that site.
  3. The event appears in the live event feed within a few seconds.
If nothing arrives, register an onError hook to see why.

Verify webhook signatures

If you subscribe to Traceten’s outbound webhooks (AI-session and conversion events), verify every delivery before you act on it. verifyWebhook() recomputes the X-Traceten-Signature HMAC in constant time, enforces the replay window, and returns the parsed, typed event, or throws WebhookVerificationError.

Verify the raw body, not a re-serialized one

The single most common verification bug: a framework parses the JSON, your code re-serializes it, and the bytes no longer match what Traceten signed (key order, whitespace, and unicode escaping all differ). Always pass the raw bytes as received, and only act on the event after verification succeeds.
  • Express: use express.raw({ type: "application/json" }) on the webhook route, not express.json(). req.body is then a Buffer you pass straight in.
  • Fastify: register a content-type parser with { parseAs: "buffer" } for the webhook route so the handler receives the raw Buffer.
toleranceSeconds (default 300) sets the replay window. Pass Infinity only when testing against the fixed conformance vector.

Troubleshooting

Events never appear. Register an onError hook. The most common causes are a wrong host, a siteId typo, or a firewall blocking outbound HTTPS. TypeError on track(). You omitted visitorId, or the event name has a hyphen. Conversion names allow only a-z, 0-9, and _. TypeError on page(). The url is not a valid absolute URL, or the eventName does not match ^[a-z][a-z0-9_-]*$. A short-lived script exits before sending. Call await traceten.close() before the process ends, or keep flushOnExit enabled.

Next