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.tgzX3_API_URL=https://api.3x.example
X3_SECRET_KEY=sk_3x_...
X3_PROJECT_ID=00000000-0000-4000-8000-000000000000Create 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.
sessionIdis required per event;pageviewIdis optional (default: the session's latest pageview);occurredAtdefaults 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(scopeevents:write,Idempotency-Keysupported).
requestContext(request)
Extracts what conversion APIs need from an incoming Request (Fetch API) or Node request:
| Field | Source |
|---|---|
clientIp | trusted forwarding headers (x-real-ip, first x-forwarded-for, cf-connecting-ip) |
clientUserAgent | user-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.