Docs

Server helper

@3x-engine/sdk/server is a small typed client for your backend. It uses a secret key, so it must never run in the browser.

npm install https://api.3x.example/v1/sdk/3x-engine-sdk-0.1.0.tgz
.env.local
X3_API_URL=https://api.3x.example
X3_SECRET_KEY=sk_3x_...
X3_PROJECT_ID=00000000-0000-4000-8000-000000000000

Create the key in the dashboard (Organization → API keys) with only the scopes the server needs, typically leads:write, conversions:write and events:write, limited to the project.

import { createX3Server, requestContext } from "@3x-engine/sdk/server";

export const x3 = createX3Server({ apiUrl: process.env.X3_API_URL!, apiKey: process.env.X3_SECRET_KEY! });

The browser's ids

Server calls are attached to the visit through the session id of the browser. Send it with the form:

// browser – npm package / <X3Provider>
import { getIds } from "@3x-engine/sdk";
body.append("x3_session", getIds().sessionId ?? "");

// browser – script tag / <X3Analytics> (window.x3 once the script has loaded)
body.append("x3_session", window.x3?.getIds?.().sessionId ?? "");

Leads

await x3.leads.create({
  sessionId,
  form: "contact",
  email, firstName, lastName, company,
  summary: "Demo request, 50 seats",
  spam: honeypotFilled,
});

Field reference: Identify and leads.

Conversions

await x3.conversions.create({
  definition: "booking",
  sessionId,
  eventId,                 // the same id as your browser pixel → platforms deduplicate
  value: 250,
  email,
  ...requestContext(request), // clientIp, clientUserAgent, fbp, fbc, ttp
});

The result is created, duplicate (same event id – nothing uploaded twice), ambiguous (when matching by match instead of sessionId) or dry_run. See Server-side conversions.

Server-side events

Events that happen on the server – a payment, a CRM stage – can be attached to the session:

await x3.events.track({ sessionId, name: "payment_succeeded", props: { plan: "pro" }, occurredAt: new Date().toISOString() });
// several at once: x3.events.track([{ … }, { … }])
  • Up to 100 events per call. sessionId is required per event; pageviewId is optional (default: the session's latest pageview); occurredAt defaults to now.
  • Unknown sessions are rejected per item (rejected: [{index, code: "session_not_found"}], HTTP 202; 422 when every item is rejected) – no synthetic pageviews are created.
  • Server events are stored with source = server, recompute the session (goals, relevance) and appear in the session timeline.
  • REST: POST /v1/projects/{project}/events (scope events:write, Idempotency-Key supported).

requestContext(request)

Extracts what conversion APIs need from an incoming Request (Fetch API) or Node request:

FieldSource
clientIptrusted forwarding headers (x-real-ip, first x-forwarded-for, cf-connecting-ip)
clientUserAgentuser-agent, control characters stripped, ≤ 500 characters
fbp, fbc, ttp_fbp, _fbc, _ttp cookies of your own domain, only when shaped correctly

These values are transient in 3x: kept only until the upload succeeded, at most 7 days.

Errors

Failures throw X3ServerError with status, code (the problem code) and the full problem object. Every call sends an Idempotency-Key (leads.create(input, key) and events.track(events, key) take your own). Conversions are deduplicated by the API on eventId: retrying with the same eventId answers duplicate and never uploads twice. Leave eventId out to confirm a held browser conversion (requireServerConfirmation) by sessionId + definition – the helper never invents one.

import { X3ServerError } from "@3x-engine/sdk/server";

try {
  await x3.leads.create({ sessionId, form: "contact", email });
} catch (error) {
  if (error instanceof X3ServerError && error.status === 429) await retryLater();
  else throw error;
}

Which project?

createX3Server({ project }) takes the project id. Without it, the helper asks the API once which projects the key can access and uses the only one – so a key limited to one project needs no id.