
@apitraffic/node is the whole ApiTraffic SDK behind a single install - the framework plugin for Express, Fastify, Koa or Hapi, the WAF and the APM bridge, mounted with one call. It captures every inbound request and response your API serves, the outbound http/https/fetch calls it makes, and (with APM) the database, cache and queue work inside each request.
It contains no telemetry logic of its own. Every feature is the same package you would otherwise install by hand, so nothing behaves differently here than it does standalone - and you can drop down to the individual packages at any time without changing how anything works.
Contents
Which package should I install?
| Everything, one install, one call | @apitraffic/node |
| Just request logging, smallest footprint | @apitraffic/express (or -fastify, -koa, -hapi) |
| Request logging + firewall, chosen individually | the framework package, plus @apitraffic/waf |
| Everything, on NestJS | @apitraffic/nestjs: this package as a Nest module |
| Everything, on Meteor | @apitraffic/meteor: this package mounted on Meteor's WebApp |
| Everything, on LoopBack 4 | @apitraffic/loopback: this package as a LoopBack component |
| Everything, on AdonisJS | @apitraffic/adonisjs: this package as an AdonisJS provider and middleware |
| Everything, on Sails | @apitraffic/sails: this package as a Sails hook |
Both routes are fully supported. This one trades a little disk for a lot less wiring. Mounting both a standalone framework package and @apitraffic/node is safe: each request is still reported once.
Requirements
- Node 20+ (
engines: >=20.0.0). The 1.x packages still run on Node 18.
- A supported framework, installed in your app. Express
^4.17.0 || ^5.0.0, Fastify ^4.0.0 || ^5.0.0, Koa ^2.13.0 || ^3.0.0 or @hapi/hapi ^21.0.0. These are optional peer dependencies: this package does not install your framework for you, and works with whichever you already have.
- An ingest token and a bucket ID from your ApiTraffic account. The setup page in ApiTraffic creates the token for you if you are an owner or admin. See API tokens.
- Outbound HTTPS from your servers to
ingest.apitraffic.io and api.apitraffic.io. Live configuration changes (redaction rules, exclusions, firewall policy) are pushed over a realtime connection to Ably, so also allow its realtime hosts; if that connection is blocked, live updates wait for a restart and nothing else is affected.
- For APM only: the OpenTelemetry packages listed under APM.
Installation
npm install @apitraffic/node
pnpm add @apitraffic/node
yarn add @apitraffic/node
Quick start
Set your credentials in the environment. Every framework call reads them when you pass no options, which keeps the token out of source control:
API_TRAFFIC_TOKEN=your-ingest-token
API_TRAFFIC_BUCKET=your-bucket-id
Node does not read .env files on its own. Start your app with node --env-file=.env server.js (Node 20.6 or later), or make require('dotenv/config'); the first line of your entry file.
Then mount ApiTraffic before your routes:
const express = require('express');
const apitraffic = require('@apitraffic/node');
const app = express();
app.use(express.json());
app.use(apitraffic.express());
app.get('/', (req, res) => res.json({ ok: true }));
app.listen(3000);
Restart the app and make a request to any route registered after ApiTraffic. The setup page in ApiTraffic moves to Traffic is flowing when the first request arrives.
ES modules and TypeScript
The package is CommonJS and works from ES modules and TypeScript as well. From an ES module, import apitraffic from '@apitraffic/node' and named imports such as import { express } from '@apitraffic/node' both work. In TypeScript:
import express from 'express';
import * as apitraffic from '@apitraffic/node';
import type { ApiTrafficOptions } from '@apitraffic/node';
const options: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };
const app = express();
app.use(express.json());
app.use(apitraffic.express(options));
Add it to your framework
Register ApiTraffic before your routes: middleware added after a route never sees the requests that route answers. On Express and Koa, mount your body parsers before ApiTraffic so the firewall's body rules can inspect the parsed body. On Fastify and Hapi the firewall runs after the body is parsed automatically.
Express
const express = require('express');
const apitraffic = require('@apitraffic/node');
const app = express();
app.use(express.json());
app.use(apitraffic.express());
app.get('/', (req, res) => res.json({ ok: true }));
apitraffic.express() returns an array of middleware (capture, then the firewall), which app.use() accepts directly.
Fastify
const fastify = require('fastify')();
const apitraffic = require('@apitraffic/node');
fastify.register(apitraffic.fastify());
The plugin is wrapped with fastify-plugin, so it applies to the whole server rather than only the scope you register it in. The firewall runs as a preHandler hook, after the body is parsed.
Koa
const Koa = require('koa');
const { bodyParser } = require('@koa/bodyparser');
const apitraffic = require('@apitraffic/node');
const app = new Koa();
app.use(bodyParser());
app.use(apitraffic.koa());
Hapi
const Hapi = require('@hapi/hapi');
const apitraffic = require('@apitraffic/node');
const init = async () => {
const server = Hapi.server({ port: 3000 });
await server.register(apitraffic.hapi());
await server.start();
};
init();
The plugin is registered with once: true, so registering it twice is skipped rather than mounting a second firewall. The firewall runs on onPreHandler, where the payload is parsed.
NestJS
Install @apitraffic/nestjs, which is this package as a Nest module, and import it once in your root module:
import { Module } from '@nestjs/common';
import { ApiTrafficModule } from '@apitraffic/nestjs';
@Module({ imports: [ApiTrafficModule.forRoot()] })
export class AppModule {}
Meteor
Install @apitraffic/meteor, which is this package mounted on Meteor's WebApp, and call it once from your server's main module:
import { mount } from '@apitraffic/meteor';
mount();
LoopBack 4
Install @apitraffic/loopback, which is this package as a LoopBack component, and register it in your application's constructor:
import { ApiTrafficComponent } from '@apitraffic/loopback';
this.component(ApiTrafficComponent);
AdonisJS
Install @apitraffic/adonisjs, which is this package as an AdonisJS provider and middleware. One command installs it and registers them:
node ace add @apitraffic/adonisjs
Sails
Install @apitraffic/sails, which is this package as a Sails hook. Sails loads it when it lifts; options go in config/apitraffic.js:
module.exports.apitraffic = {
token: process.env.API_TRAFFIC_TOKEN,
bucket: process.env.API_TRAFFIC_BUCKET,
};
Placing the pieces yourself
apitraffic.parts('express' | 'fastify' | 'adonisjs', options) returns the pieces express() and fastify() mount, and their AdonisJS equivalents, for an integration that has to put them in place itself (@apitraffic/nestjs, @apitraffic/meteor, @apitraffic/loopback, @apitraffic/adonisjs and @apitraffic/sails use it):
adapter - the framework package, set up as the one the helpers act through (or undefined if it could not load);
options - your options without this package's own waf key, for adapter.middleware(options);
firewall - to mount where the body is parsed (Express middleware, a Fastify preHandler hook, or an AdonisJS middleware (ctx, next)).
Mount adapter.middleware first, then firewall. For adonisjs the adapter is @apitraffic/express, run on the raw Node request and response: mount it as adapter.middleware(options, { view }), with a view(req) that reads what Express would know (ip, protocol, url, method, body, route) from the Adonis context, as @apitraffic/adonisjs does.
Configuration
Options are passed to the framework call (apitraffic.express({ ... }), apitraffic.fastify({ ... }), …) or set as API_TRAFFIC_* environment variables. Most applications need only API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET. Everything that decides what is captured, redacted or blocked is set in your ApiTraffic account and pushed to the SDK, not configured here.
Precedence
An option passed in code wins over its environment variable, which wins over the built-in default. You can mix the two - the token from the environment and debug in code, say. Settings from your ApiTraffic account (redaction rules, exclusions, the firewall policy, APM capture switches, host telemetry) load at startup and update in real time, without a restart.
A few settings follow a different rule:
| offlineWafMode | API_TRAFFIC_OFFLINE_WAF_MODE wins over the code option, so you can change it during an incident without a deploy. Only exactly closed (lowercase) means closed: any other non-empty value, such as CLOSED, still overrides the option and fails open. |
| Firewall on or off | Off if either waf: false is passed or API_TRAFFIC_WAF_ENABLED is exactly false or 0, or off in any case. This variable does not use the switch parser below: FALSE, no or a value with spaces leaves the firewall on. Neither can turn on a firewall your account has not enabled. |
| captureStatementParams, captureRedisKeys | On if your code or environment turns it on, or the bucket's APM Capture settings do. The dashboard can add capture, never remove what your code asked for. |
| captureCallSite | The bucket's APM Capture setting applies unless you set it in code or the environment. Set locally it can only turn code location on for the SDK's own spans: a local false does not override a bucket that has it on. The APM bridge reads API_TRAFFIC_CAPTURE_CALL_SITE (or its own captureCallSite option) itself, so a false there does turn it off for OpenTelemetry spans. |
| captureStackDepth | The bucket's APM Capture setting applies unless you set it in code or the environment, which then wins. |
| interceptOutbound | A blank value ("", null, or an empty environment variable) turns outbound capture off. For the other switches, limits, sample rates, token, bucket and accountSid, a blank value counts as not set (apiHost and ingestHost fall back only when empty; a value of spaces is used as given). Two exceptions: passing debug at all, even as undefined, overrides API_TRAFFIC_DEBUG, and a blank projectRoot in code skips API_TRAFFIC_PROJECT_ROOT. |
Switches accept true/false, yes/no, on/off or 1/0 in any case, as strings or booleans; byte caps and counts must be whole numbers greater than 0 (captureStackDepth may also be 0); sample rates are numbers from 0 to 1. An invalid value is reported once on the console and ignored. debug and strict are not parsed from strings in code - pass true or false - and API_TRAFFIC_DEBUG turns debug logging on only when it is exactly true (any case). The bundled APM bridge is the exception: it also reads 1, yes and on as on.
Credentials and connection
| token | API_TRAFFIC_TOKEN | string | - | Required. Ingest token from your ApiTraffic account. Without it the SDK logs an error and disables telemetry; your app still starts. |
| bucket | API_TRAFFIC_BUCKET | string | - | Required. The bucket captured traffic is sent to. |
| accountSid | API_TRAFFIC_ACCOUNT_SID | string | Learned from your account settings | Only needed by a process that mints embed tokens without running the ApiTraffic middleware. |
| apiHost | API_TRAFFIC_API_HOST | string | api.apitraffic.io | Host the SDK loads its settings from. A bare host name means https://. |
| ingestHost | API_TRAFFIC_INGEST_HOST | string | ingest.apitraffic.io | Host captured traffic is sent to. A bare host name means https://. |
| debug | API_TRAFFIC_DEBUG | boolean | false | Log SDK activity to the console. |
| strict | - | boolean | false | Makes the base client's setup() throw on misconfiguration (missing token or bucket) instead of logging. This package runs setup in the background and catches that error, so strict does not stop your app from booting, and the missing-token message is then printed only when API_TRAFFIC_DEBUG is set. Leave it off. |
Delivery honours the standard HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY environment variables. The SDK warns once if apiHost or ingestHost is plain http:// and not this machine.
Capture and delivery
| interceptOutbound | API_TRAFFIC_INTERCEPT_OUTBOUND | switch | true | Capture outbound calls made with http, https and fetch. Off leaves those modules untouched. |
| captureFetch | API_TRAFFIC_CAPTURE_FETCH | switch | true | Capture outbound global fetch calls (requires interceptOutbound). Telemetry only: an egress firewall policy still applies to fetch. |
| redactSensitiveKeys | API_TRAFFIC_REDACT_SENSITIVE_KEYS | switch | true | Mask credential-shaped body keys and query parameters (password, token, secret, api_key, …) in addition to your redaction rules. |
| maxBodyBytes | API_TRAFFIC_MAX_BODY_BYTES | bytes | 524288 (512 KB) | Largest request or response body sent. A larger body is cut to the cap and ends with …[truncated by ApiTraffic: N bytes total]. Applied after redaction. |
| maxRedactBodyBytes | API_TRAFFIC_MAX_REDACT_BODY_BYTES | bytes | 1048576 (1 MB) | Largest body the SDK redacts. A larger body is replaced by a placeholder before any redaction work. |
| maxFetchBodyLength | API_TRAFFIC_MAX_FETCH_BODY_LENGTH | bytes | 524288 (512 KB) | Largest outbound request or response body captured. |
| requestSampleRate | API_TRAFFIC_REQUEST_SAMPLE_RATE | rate | 1 | Fraction of captured requests sent. 5xx responses are always sent. |
| ingestBatch | API_TRAFFIC_INGEST_BATCH | switch | true | Send several payloads per request when your account and the ingest host support it. |
| ingestGzip | API_TRAFFIC_INGEST_GZIP | switch | true | Compress those batches when your account asks for it. |
Spans and APM
These control the spans recorded inside each request - from the APM bridge and from your own span() / startSpan() calls.
| captureSpans | API_TRAFFIC_CAPTURE_SPANS | switch | true | Master switch for spans and per-request span rollups. |
| captureStatements | API_TRAFFIC_CAPTURE_STATEMENTS | switch | true | Include the normalized SQL or command text (where id = ?) in spans. |
| captureStatementParams | API_TRAFFIC_CAPTURE_STATEMENT_PARAMS | switch | false | Also keep statements as issued, with their bind values. These carry your users' data. |
| captureRedisKeys | API_TRAFFIC_CAPTURE_REDIS_KEYS | switch | false | Include the concrete Redis key, unmasked and not run through redaction rules. |
| captureCallSite | API_TRAFFIC_CAPTURE_CALL_SITE | switch | true (bucket setting) | Record the application code location that started each span. Unset follows the bucket. |
| captureStackDepth | API_TRAFFIC_CAPTURE_STACK_DEPTH | whole number ≥ 0 | 10 (bucket setting) | Application frames kept per span; 1 keeps the call site alone. |
| projectRoot | API_TRAFFIC_PROJECT_ROOT | string | Detected | Directory code locations are reported relative to. |
| redisKeyPatterns | - | array of strings or RegExps | [] | Patterns for Redis keys whose variable parts the automatic masking cannot infer. |
| maxSpansPerRequest | API_TRAFFIC_MAX_SPANS_PER_REQUEST | count | 500 | Spans kept per request. Work past the cap is still counted in rollups. |
| spanSampleRate | API_TRAFFIC_SPAN_SAMPLE_RATE | rate | 1 | Fraction of requests that keep full span detail. |
| maxSpanMetaBytes | API_TRAFFIC_MAX_SPAN_META_BYTES | bytes | 2048 | Size cap on each span's metadata, after redaction. |
The APM preload (register()) has options of its own - enabledInstrumentations, disabledInstrumentations, instrumentHttp, serviceName, force and more. They are listed in the @apitraffic/apm README and the configuration reference.
Firewall
| waf | API_TRAFFIC_WAF_ENABLED | false | object | Unset | Local override for the firewall. false, or the variable set to exactly false or 0 (or off in any case), refuses it whatever the dashboard says. An object passes settings to @apitraffic/waf (and enabled: false in it turns the firewall off too). true is accepted and means the same as unset. Normally left unset - see Enabling features. |
| waf.offlineWafMode | API_TRAFFIC_OFFLINE_WAF_MODE | 'inherit' | 'open' | 'closed' | 'inherit' | Passed to @apitraffic/waf. In this package it has no practical effect at startup: the firewall is only loaded once a policy is available (live, cached or pinned), so a server that has never loaded one serves every request unfiltered whatever this says. The variable wins over the option. |
| waf.onEvent | - | function | - | Called for every inbound block or detection. |
| waf.onEgressEvent | - | function | - | Called for every outbound block or detection. |
| waf.debug | - | boolean | The SDK's debug | Log firewall errors to the console (a policy that fails to compile, an evaluation that throws); decisions go to waf.onEvent. Unset, it follows the SDK's own debug setting. |
const apitraffic = require('@apitraffic/node');
app.use(apitraffic.express({
waf: {
onEvent: (event) => logger.warn({ waf: event }, 'firewall event')
}
}));
Note: with @apitraffic/node the firewall fails open until the first policy is available. A server that starts while ApiTraffic is unreachable, with no cached or pinned copy of your policy, is not protected until a policy loads. To have a policy at boot, commit offline definitions. To refuse traffic until one loads (offlineWafMode: 'closed'), mount @apitraffic/waf directly instead.
Offline definitions and server labels
API_TRAFFIC_DEFINITIONS_PATH | Unset | Path of the pinned definitions file (normally .apitraffic/definitions/sdk.json). When set, it is the only place the SDK looks. |
API_TRAFFIC_CACHE_DIR | node_modules/.cache/apitraffic | Where the SDK keeps its last-known-good copy of your settings. |
API_TRAFFIC_FALLBACK_MAX_AGE_DAYS | Unset (no limit) | Skip a pinned file older than this many days. |
API_TRAFFIC_SERVICE_NAME, API_TRAFFIC_SERVICE_VERSION, API_TRAFFIC_REGION, API_TRAFFIC_GIT_SHA | Unset | Label each server and the requests it serves. Common platform variables (AWS_REGION, GIT_SHA, VERCEL_GIT_COMMIT_SHA, …) are used as fallbacks. |
Every option, its parsing rules and the full fallback lists are in the SDK configuration reference.
Enabling features
There is nothing to switch on in code. Everything is mounted by the calls above, and what actually runs is decided in ApiTraffic and pushed to every connected server - the WAF, data redaction, request exclusions, span capture and host telemetry are all controlled that way. Turning a feature on is a change in the dashboard, not a deploy.
Mounting is not the same as loading. The WAF engine is only loaded once a live policy arrives, so an account that never enables it pays a single null check per request - no rules in memory, no boot-time cost.
Overriding locally
Two switches take precedence over the dashboard, and exist for the situations a dashboard cannot cover. Both turn the firewall off; neither can turn it on:
app.use(apitraffic.express({ waf: false }));
API_TRAFFIC_WAF_ENABLED=false
Use the first when your change process requires that anything able to reject production traffic ships through a deploy rather than a click. Use the second during an incident - it works even when ApiTraffic is unreachable, including when ApiTraffic is the problem.
APM
APM is the one piece this cannot fold into the call above, and the reason is unavoidable: OpenTelemetry instruments libraries by patching them as they are required, so it has to run before your application requires pg, redis, http and the rest. By the time a middleware factory runs, that moment has passed.
So it needs a preload. OpenTelemetry is an optional peer of @apitraffic/apm, which this package depends on (its README lists the supported ranges) - install it when you want APM:
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @opentelemetry/auto-instrumentations-node
Then start your app with:
node -r @apitraffic/node/register app.js
With credentials in a .env file: node --env-file=.env -r @apitraffic/node/register app.js. The preload reads its configuration from API_TRAFFIC_* environment variables, because a preload has nowhere to take arguments from.
If you cannot change the start command, make this the very first line of your entry file, above every other require - it also lets you pass options in code:
require('@apitraffic/node').register({ serviceName: 'orders-api' });
register() returns { registered, reason?, loadedTooEarly? } and never throws. If the OpenTelemetry packages are missing or fail to start, APM is skipped - with a one-line reason on the console - and your application boots normally. If your database or cache client was already loaded when register() ran, it produces no spans, and the console says which modules were loaded too early.
ES modules start with node --import and OpenTelemetry's loader hook instead of -r:
import { register } from 'node:module';
register('@opentelemetry/instrumentation/hook.mjs', import.meta.url);
node --import ./otel-hook.mjs --import @apitraffic/node/register ./server.mjs
This preload is for applications with no other tracing: it owns the process's OpenTelemetry tracer provider and installs only a curated set of instrumentations, with no exporters or propagators. If your application already runs OpenTelemetry, skip the preload and add ApiTrafficSpanProcessor from @apitraffic/apm to your own provider; with Datadog or New Relic loaded the preload steps aside on its own. See the @apitraffic/apm README and Set up APM.
What gets captured
- Inbound requests your framework serves after ApiTraffic is mounted: method, URL, headers, query string, request and response bodies, status and timing. Aborted requests are reported with status
499.
- Outbound calls made with
http, https (including libraries built on them, such as axios and got) and the global fetch, with their request and response bodies (up to maxFetchBodyLength, 512 KB by default). Switch them off with interceptOutbound or captureFetch. See Outbound requests.
- Spans from the APM bridge (queries, cache calls, queues, RPC, outbound calls the SDK cannot see itself) and from your own
span() / startSpan() calls, plus per-request rollups.
- Tags and traces you add with
tag() and trace(), and the firewall's decision on each request it matched.
Bodies are capped: over maxBodyBytes a body is cut to the cap and marked as truncated (over maxRedactBodyBytes it is replaced by a placeholder), and a body written in pieces or streamed (res.write(), Server-Sent Events, a stream returned from a handler) is captured from the bytes written, up to maxBodyBytes; the framework package READMEs list the few exceptions. Bodies that cannot be captured safely (nested more than 256 levels deep, unreadable, or not processable by your redaction rules) are replaced with a [body not captured: …] placeholder, and the rest of the request is still sent. requestSampleRate sends a fraction of requests; server errors are always sent.
Data security
- Redaction happens in your process. Your account's redaction rules are applied before anything leaves the server, and nothing is sent before they have loaded.
- Sensitive keys are masked by default. Credential-shaped body keys and query parameters (
password, token, secret, api_key, …) are redacted whatever your own rules say; credential headers are always masked. Turn the key masking off with redactSensitiveKeys: false / API_TRAFFIC_REDACT_SENSITIVE_KEYS=false.
- A request whose redaction cannot be completed is dropped, never sent unredacted.
- Exclusions stop matching requests from being sent at all, configured in the dashboard with no code change. See Exclusions.
- SQL values stay in your process unless you turn on
captureStatementParams; spans carry the normalized statement (where id = ?).
Treat the ingest token as a secret. It can't read captured traffic or mint embed tokens, but whoever holds it can send traffic into your bucket and read the SDK's settings, which include your redaction rules and firewall policy. Keep it in a secret store, never in client-side code, images or a repository, and rotate it if it may have been exposed.
Tagging, tracing and spans
const apitraffic = require('@apitraffic/node');
app.post('/v1/checkout', async (req, res) => {
apitraffic.tag('accountId', 'acct_123');
apitraffic.trace('reached the discount branch');
const order = await apitraffic.span('custom', 'price cart', () => priceCart(req.body));
res.json(order);
});
tag(key, value) | Attach a searchable key-value pair. Keys are 1–64 characters (a letter first, then letters, digits, _, . or -); values longer than 255 characters are cut; at most 32 tag keys per request. See the tag rules. |
trace(message) | Record a timestamped note (up to 255 characters, 250 per request; a longer message is dropped, not cut). |
span(type, name, fn, meta?) | Run fn, time it, and record it as a span. Returns what fn returns (sync or promise) and re-throws its error after marking the span failed. time() is the same function. |
startSpan(type, name, meta?) | Start a span for work that does not fit one function. Returns a handle with setMeta(meta) and end({ ok?, meta? }). |
addSpan(span) | Add an already-built span to the current request. |
getRequestManager() / getContext() | The current request's state, or undefined outside a request. Most applications never need them. |
type is one of db.query, redis, cache, http.out, queue, rpc, graphql or custom. All of these are safe to call from anywhere. Outside a request they do nothing rather than throwing, and a tag that breaks a rule is dropped and counted, never thrown into your handler.
Embed tokens
The embeddable request log, and embedded reports and dashboards, authenticate with a short-lived JWT that locks in the bucket, the filters and what the embed may show. It is minted on your server with an Administrator API token with no access scopes, set as API_TRAFFIC_EMBED_TOKEN (the ingest token the SDK reports with cannot mint embed tokens), and this package mints it:
const apitraffic = require('@apitraffic/node');
app.get('/api/embed-token', requireUser, async (req, res, next) => {
try {
const { token, expiresAt } = await apitraffic.embedToken({
filters: { tags: { customerId: req.user.id } },
ttl: 900
});
res.json({ token, expiresAt });
} catch (err) {
next(err);
}
});
The host, bucket and account come from the SDK you already configured and the token from API_TRAFFIC_EMBED_TOKEN, so a call names only what is specific to this embed. Everything is optional, except reportSid or dashboardSid for a report or dashboard embed:
| bucketSid | The bucket the SDK reports to (request log only) | The bucket the embed is scoped to. A report or dashboard keeps its own scope unless you pass one. |
| view | 'request-log' | 'request-log', 'report' (with reportSid) or 'dashboard' (with dashboardSid). The report or dashboard must allow embedding. |
| filters | {} | environmentSid, criteria, streamViewSid, tags (request log) or filter, environmentSid, tags (report and dashboard) - signed into the token, so the browser cannot widen them. tags ({ customerId: id }) is written into the criteria (for a report or dashboard, the filter) safely; if you build criteria yourself, put values in double quotes through escapeCriteriaValue(). |
| timeRange / timeRanges | The report's own / all | Report and dashboard embeds: the range the embed opens with ({ relative: 'last_7d' }) and the ranges the viewer may pick. |
| ttl | 3600 | Token lifetime in seconds (60–86400). |
| theme | 'light' | 'light', 'dark' or 'auto'. |
| tabs | The API's default set | Request log embeds only (the API refuses it for a report or dashboard): the request detail tabs it may show. [] for a list with no expandable detail. |
| features | {} | Request log embeds only: copyRequest, includeBodies. |
| accountSid | From your SDK settings | Only needed in a process that does not run the ApiTraffic middleware. Also read from API_TRAFFIC_ACCOUNT_SID. |
| token / apiHost | API_TRAFFIC_EMBED_TOKEN, then the SDK's own | Override the credential or host for one call. If it falls back to the SDK's ingest token, the API answers 403. |
It resolves with { token, expiresAt, tabs, features }. Data behind a tab you do not grant is never returned to the browser - the tabs are an access boundary, not a display setting. See the embed documentation for the full list.
This is the one call in the package that rejects rather than going quiet. A token has a single useful outcome, and it is produced inside your own await, where a failure is a 500 on that route instead of a broken embed with nothing to explain it.
API_TRAFFIC_EMBED_TOKEN is a read credential. An Administrator API token can mint embed tokens for any bucket in its account, with any filters or none, so whoever holds it can read all of the account's captured traffic. Keep it in a secret store (never in client-side code, images or a repository), set it only on servers that mint embed tokens, and rotate it immediately if it may have been exposed.
Delivery health
const apitraffic = require('@apitraffic/node');
console.log(apitraffic.stats());
Useful when you want to assert in your own monitoring that telemetry is actually leaving the process: payloads in flight and queued, delivered, recovered after a retry, dropped by cause, and whether the circuit breaker is open. The same counters are reported to ApiTraffic in real time.
Shutting down
The SDK keeps a realtime connection open for live configuration, which is right for a server and wrong for a script, a queue worker or a test run that is meant to finish. Stop it when you are done:
const server = app.listen(3000);
process.once('SIGTERM', async () => {
await new Promise((resolve) => server.close(resolve));
await apitraffic.shutdown({ flushTimeoutMs: 2000 });
process.exit(0);
});
shutdown() stops capturing, delivers what is in flight and queued (waiting at most flushTimeoutMs, default 2 s; whatever is left is dropped and counted), closes the realtime connection, stops background timers and removes the http/https/fetch hooks. It also stops APM tracing that register() started. It installs no signal handlers - you decide when - never throws, resolves with { flushed, remaining }, and a later setup starts the SDK again.
To deliver what has been captured without stopping - before a serverless function is frozen, say - use await apitraffic.flush({ timeoutMs: 1000 }) (default 2 s). Anything not delivered in time stays queued.
Setup never waits for the realtime connection: where it is blocked, live config updates are off and nothing else is affected. See Shutting down.
TypeScript
Declarations ship with the package. ApiTrafficOptions types every option, including waf; a value of the wrong type, or a misspelled key in an object literal, is a compile error, and at runtime unknown keys are still passed through.
import type { ApiTrafficOptions } from '@apitraffic/node';
const options: ApiTrafficOptions = { debug: true, waf: { offlineWafMode: 'open' } };
Switches and limits accept a string too, so { captureSpans: process.env.CAPTURE_SPANS } type-checks; debug and strict stay boolean.
ApiTrafficOptions | Options for every framework call: ApiTrafficSetupOptions plus waf. |
ApiTrafficSetupOptions | The base SDK options, re-exported from @apitraffic/utilities. |
WafConfigureOptions | The object form of waf, re-exported from @apitraffic/waf. |
ApmOptions, RegisterResult | Options and result of register(), re-exported from @apitraffic/apm. |
ApiTrafficExpressHandler, ApiTrafficFastifyPlugin, ApiTrafficKoaMiddleware, ApiTrafficHapiPlugin | What each framework call returns. |
ApiTrafficParts | What parts() returns. |
SpanHandle, SpanMeta | The handle startSpan() returns, and span metadata. |
EmbedTokenOptions, EmbedTokenResult | Options and result of embedToken(). |
Failure behaviour
The SDK is built so that it can never be the reason your application fails.
- A missing or invalid token disables telemetry and logs a clear message. Your application still boots.
- If ApiTraffic is unreachable, requests are served normally. A short outage is absorbed by an in-memory retry queue; a longer one sheds data rather than growing without bound. See If ApiTraffic is unreachable.
- If the realtime connection fails, you lose live config updates and nothing else.
- Nothing is sent before your account's client-side redaction rules have loaded.
- If a bundled package cannot load, that feature is disabled with a one-line console message and the rest of the SDK keeps working.
tag() and trace() never throw into your handlers, and every helper does nothing outside a request.
Want the app to refuse to boot without credentials? Check API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET yourself before you start the server. strict: true does not do it here: setup runs in the background and its error is caught.
Troubleshooting
Set API_TRAFFIC_DEBUG=true and restart: the SDK explains what it is doing on the console.
The setup page never leaves "Listening for your application". The SDK has not connected. Check that the process was restarted after you set API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET, and that the variables actually reach the process (Node does not load .env by itself). Even without debug mode the SDK prints ApiTraffic: An APITraffic API Token must be set… for a missing token, and ApiTraffic: token rejected (HTTP 401)… or (HTTP 403) for one that is invalid or cannot access the bucket. Make sure outbound HTTPS to api.apitraffic.io and ingest.apitraffic.io is allowed, and that any proxy is set in HTTPS_PROXY.
SDK connected, but no requests appear. ApiTraffic is registered after the route you are calling, the request is answered by middleware mounted before ApiTraffic (a health check or static file handler), or an exclusion matches it.
The firewall does not inspect request bodies. On Express and Koa, mount your body parser before apitraffic.express() or apitraffic.koa(). If a router has its own body parser mounted after ApiTraffic, mount apitraffic.express() (or apitraffic.koa()) again on that router, after its parser: the firewall then evaluates the request again with the body, without counting it twice against rate limits or reporting it twice.
APM shows no spans. APM needs the preload (node -r @apitraffic/node/register) or register() as the first line of your entry file, and the OpenTelemetry packages installed. Check the console: register() prints why it did not start and which modules were already loaded.
The installed @apitraffic/waf has no firewall for this framework. Two different versions of the SDK packages are installed. The server SDK packages (@apitraffic/node, the framework packages, @apitraffic/waf, @apitraffic/apm and @apitraffic/utilities) share one version number from 2.0.0 on, so align their versions (for example with npm ls @apitraffic/waf).
My script or test run never exits. The realtime connection keeps the event loop alive. Call await apitraffic.shutdown() when the work is done.
Documentation and related packages
Changelog and license
Release notes are in CHANGELOG.md. Released under the MIT License.
Follow @apitraffic/node on npm to stay updated on new releases.
Follow us on our Blog and on X.
Chat with the ApiTraffic team and other members on Discord and follow our tutorials and other videos on YouTube.



How to contribute
Here are some ways you can help improve ApiTraffic:
- Give ApiTraffic a try and share your feedback to help make ApiTraffic even better for you. Feel free to reach out to us on Discord!
- Join our Discord to connect with other members, share your experiences, and learn from one another.
- Report bugs or request features by emailing support@apitraffic.io or asking on Discord.