> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traffical.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js SDK

> Server-side SDK for Node.js and Bun — install, initialize, resolve parameters, track events.

`@traffical/node` is the server-side SDK for Node.js and Bun. It fetches the config bundle on startup, resolves parameters locally per request, and ships events to Traffical in the background.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @traffical/node
  ```

  ```bash pnpm theme={null}
  pnpm add @traffical/node
  ```

  ```bash yarn theme={null}
  yarn add @traffical/node
  ```

  ```bash bun theme={null}
  bun add @traffical/node
  ```
</CodeGroup>

## Initialization

```typescript theme={null}
import { createTrafficalClient } from "@traffical/node";

const traffical = await createTrafficalClient({
  orgId: "org_acme",
  projectId: "proj_marketplace",
  env: "production",
  apiKey: process.env.TRAFFICAL_API_KEY!,
});
```

Use `createTrafficalClient` (async) when you can `await` at startup — it waits for the first bundle fetch. If you can't await (e.g. in module top-level code where top-level await isn't available), use `createTrafficalClientSync` and call `await traffical.waitForReady()` before the first resolution. `waitForReady()` resolves once the first config load completes — and still resolves (never hangs) when the SDK fails open on an unavailable or malformed bundle.

### Options

| Option                       | Type                   | Default                    | Description                                                                                                           |
| ---------------------------- | ---------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `orgId`                      | `string`               | required                   | Organization ID                                                                                                       |
| `projectId`                  | `string`               | required                   | Project ID                                                                                                            |
| `env`                        | `string`               | required                   | Environment name (`"production"`, `"staging"`, etc.)                                                                  |
| `apiKey`                     | `string`               | required                   | SDK key (`traffical_sk_...`)                                                                                          |
| `baseUrl`                    | `string`               | `https://sdk.traffical.io` | SDK API base URL                                                                                                      |
| `refreshIntervalMs`          | `number`               | `60000`                    | How often to re-fetch the bundle                                                                                      |
| `localConfig`                | `ConfigBundle`         | —                          | Embed a bundle for instant cold-start                                                                                 |
| `evaluationMode`             | `"bundle" \| "server"` | `"bundle"`                 | Resolve locally from the bundle, or delegate to the edge per call. See [how it works](/how-it-works#evaluation-modes) |
| `batchSize`                  | `number`               | `10`                       | Events per delivery batch before flushing                                                                             |
| `flushIntervalMs`            | `number`               | `30000`                    | Max time before flushing pending events                                                                               |
| `eventMaxQueueSize`          | `number`               | `1000`                     | Max events buffered before the oldest is dropped                                                                      |
| `configTimeoutMs`            | `number`               | `10000`                    | Timeout for the config-bundle fetch; a timeout is treated as a network failure                                        |
| `eventsTimeoutMs`            | `number`               | `10000`                    | Timeout for event-delivery POSTs; on timeout the batch is re-queued for retry                                         |
| `resolveTimeoutMs`           | `number`               | `5000`                     | Timeout for server-mode resolve calls (`POST /v1/resolve`)                                                            |
| `trackDecisions`             | `boolean`              | `true`                     | Emit decision events alongside exposures                                                                              |
| `decisionDeduplicationTtlMs` | `number`               | `3600000`                  | Window in which the same unit + assignment won't re-emit a decision event                                             |
| `deduplicateExposures`       | `boolean`              | `true`                     | Dedup exposure events per (unit, policy, allocation) within a session                                                 |
| `exposureSessionTtlMs`       | `number`               | `1800000`                  | Exposure-dedup session TTL (30 min)                                                                                   |
| `disableCloudEvents`         | `boolean`              | `false`                    | Stop sending events to Traffical (config is still fetched)                                                            |
| `assignmentLogger`           | `AssignmentLogger`     | —                          | Route assignment rows to your own warehouse pipeline                                                                  |

<Note>
  **Deprecated option aliases.** Earlier releases used `eventBatchSize`, `eventFlushIntervalMs`, and a single `requestTimeoutMs`. These still work and forward to the canonical names above (`batchSize`, `flushIntervalMs`, and the three-way timeout split). `requestTimeoutMs` is honored as the legacy fallback for any of `configTimeoutMs` / `eventsTimeoutMs` / `resolveTimeoutMs` that you don't set explicitly. Prefer the canonical names — the aliases will be removed in a future major.
</Note>

## Resolving parameters

```typescript theme={null}
const params = traffical.getParams(
  { userId: "user_789", locale: "en-US", plan: "pro" },
  {
    "checkout.button.color": "#1E6EFB",
    "checkout.headline": "Complete your order",
    "checkout.show_trust_badges": false,
    "pricing.discount_pct": 0,
  },
);

params["checkout.button.color"];     // "#22C55E" if assigned to treatment
params["checkout.show_trust_badges"]; // true if policy overrides it
params["pricing.discount_pct"];       // 0 (no policy → default)
```

Both `getParams(context, defaults)` and `decide(context, defaults)` take **context first, defaults second**, and both are **synchronous**.

<Note>
  The legacy object-bag form — `getParams({ context, defaults })` / `decide({ context, defaults })` — still works but is deprecated. Prefer the positional form above.
</Note>

### Context

The `context` object is used for:

* **Bucketing** — the unit key (typically `userId`, as configured on the [project](/concepts/projects-and-environments)) drives which allocation the user gets.
* **Targeting** — every field is available for policy condition evaluation. A policy with the condition `plan in ["pro", "enterprise"]` only applies when `context.plan` is one of those values.

### Defaults

Always pass defaults for every parameter you read. They're the fallback when no policy matches, when the bundle isn't loaded yet, or when Traffical is unreachable — and they make your code's intent explicit even when there's no active experiment.

## Decisions vs `getParams`

`getParams` returns just the resolved values. `decide` (also synchronous) returns a full `DecisionResult` — a `decisionId`, the resolved `assignments`, and per-decision `metadata` (unit key value, per-layer resolution rows, config version) — useful when you want to pass the decision ID downstream:

```typescript theme={null}
const decision = traffical.decide(
  { userId: req.user.id },
  { "recommendations.algorithm": "collaborative" },
);

const recs = await getRecommendations(req.user.id, decision.assignments["recommendations.algorithm"]);

res.json({
  recommendations: recs,
  meta: { decisionId: decision.decisionId },   // frontend can attribute clicks back to this decision
});
```

See [decisions & attribution](/concepts/assignments) for when this matters.

## Tracking events

`track(event, properties?, options?)` — the event name first, an optional `properties` payload second, and an options bag third:

```typescript theme={null}
// Conversion with a value
traffical.track(
  "purchase",
  { order_total: 49.99, currency: "USD" },
  { unitKey: "user_789", value: 49.99 },
);

// Action without a value
traffical.track("add_to_cart", {}, { unitKey: "user_789" });

// Cross-process — attribute to a specific decision
traffical.track(
  "page_view",
  { path: "/checkout" },
  { unitKey: "user_789", decisionId: "dec_abc123" },
);
```

### Track arguments

| Argument     | Type                | Description                                    |
| ------------ | ------------------- | ---------------------------------------------- |
| `event`      | `string`            | Event name (positional, required)              |
| `properties` | `object`            | Event payload (positional, optional)           |
| `options`    | `TrackEventOptions` | Options bag (positional, optional) — see below |

`TrackEventOptions` fields:

| Field            | Type                     | Description                                                                                                 |
| ---------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `unitKey`        | `string`                 | The user ID (or whatever the project's unit key is). On the server there is no auto stable ID, so pass this |
| `decisionId`     | `string`                 | Tie this event to a specific decision (cross-process)                                                       |
| `value`          | `number`                 | Single numeric value (e.g. revenue); falls back to `properties.value`                                       |
| `values`         | `Record<string, number>` | Multiple named numeric values for multi-objective optimization                                              |
| `eventTimestamp` | `string`                 | Explicit event time (ISO 8601); defaults to "now"                                                           |

## Express middleware pattern

```typescript theme={null}
import express from "express";
import { createTrafficalClient } from "@traffical/node";

const app = express();

const traffical = await createTrafficalClient({
  orgId: "org_acme",
  projectId: "proj_marketplace",
  env: "production",
  apiKey: process.env.TRAFFICAL_API_KEY!,
});

app.use((req, res, next) => {
  const userId = req.session?.userId ?? req.cookies?.anonymousId;
  if (userId) {
    req.traffical = traffical.getParams(
      { userId, locale: req.headers["accept-language"] },
      {
        "checkout.button.color": "#1E6EFB",
        "checkout.show_trust_badges": false,
      },
    );
  }
  next();
});

app.get("/checkout", (req, res) => {
  res.render("checkout", {
    buttonColor: req.traffical["checkout.button.color"],
    showBadges: req.traffical["checkout.show_trust_badges"],
  });
});
```

## Type-safe events

Generate TypeScript types from your event definitions to catch invalid event names and properties at compile time:

```bash theme={null}
bunx @traffical/cli generate-types
```

Instantiate the client with your generated event map and `track` is type-checked against it — event names and their property shapes:

```typescript theme={null}
import { createTrafficalClient } from "@traffical/node";
import type { TrafficalEventProperties } from "./traffical.generated";

const traffical = await createTrafficalClient<TrafficalEventProperties>({
  orgId, projectId, env, apiKey,
});

traffical.track(
  "purchase",
  { order_total: 99.99, payment_method: "visa" },   // ✅
  { unitKey: "user_789" },
);

traffical.track(
  "purchase",
  { order_total: 99.99, unknown_field: true },       // ❌ compile error
  { unitKey: "user_789" },
);
```

## Error handling

The SDK never throws during resolution. If the bundle isn't loaded yet — or if it never loaded successfully — `getParams` returns your defaults. Initialization errors surface as a rejected promise:

```typescript theme={null}
const traffical = await createTrafficalClient({
  orgId, projectId, env, apiKey,
}).catch((err) => {
  console.error("Failed to initialize Traffical:", err);
  return null;
});
```

You can keep a `null` client around and guard your calls — but more often it's cleaner to use `createTrafficalClientSync`, which returns a usable client immediately (resolution falls back to defaults until the bundle arrives) and never rejects at construction.

## Shutdown

Flush pending events before your process exits:

```typescript theme={null}
await traffical.close();
```

This sends any buffered events and stops the background refresh.

## Next steps

<CardGroup cols={2}>
  <Card title="A/B testing" icon="flask" href="/experimentation/ab-testing">
    Run a static experiment.
  </Card>

  <Card title="Canonical experiments" icon="book-open" href="/guides/canonical-experiments">
    Patterns for backend, batch, and cross-surface tests.
  </Card>

  <Card title="CLI" icon="terminal" href="/tools/cli">
    Manage parameters as code.
  </Card>

  <Card title="API reference" icon="code" href="/api/overview">
    The endpoints the SDK calls.
  </Card>
</CardGroup>
