---
title: "@togohq/sdk-node"
description: "Every method and option of the server SDK, and how to make sure events leave before the process stops."
canonical: "https://www.togohq.ai/docs/reference/sdk-node"
applies_to: "@togohq/sdk-node 1.x"
updated: "2026-11-05T00:00:00Z"
---

# @togohq/sdk-node

Every method and option of the server SDK, and how to make sure events leave before the process stops.

```bash
npm install @togohq/sdk-node
```

Node 20 or later, zero runtime dependencies. Create **one client per process**, shared by every
request. Nothing on the client throws: a problem is logged as `[togo] …` and the event is retried
or dropped. A guide to using it: [Node.js and serverless](/docs/node).

## `init(options)`

```ts
import { init } from '@togohq/sdk-node';

const analytics = init({ writeKey: process.env.TOGO_WRITE_KEY! });
```

## Options

| Option                 | Default                | What it does                                                                     |
| ---------------------- | ---------------------- | -------------------------------------------------------------------------------- |
| `writeKey`             | —                      | Required. `togo_pkt_…` (test) or `togo_pkl_…` (live)                             |
| `endpoint`             | `https://in.togohq.ai` | Where events are sent                                                            |
| `batchSize`            | `100`                  | Events per request, 100 to 500                                                   |
| `flushIntervalSeconds` | `10`                   | How long an event may wait before it is sent, 10 to 60 seconds                   |
| `maxQueueSize`         | `10000`                | Events held in memory while Togo is unreachable; past it, new events are dropped |
| `defaultProperties`    | —                      | Properties on every `track()` event, such as `{ service: 'billing' }`            |

A `batchSize` or `flushIntervalSeconds` outside its range is moved into it, with a warning in your
logs. To send an event at once, `await analytics.flush()` after it rather than lowering either.

`defaultProperties` is fixed when the client is created, because a client shared by every request
must not carry one request's value onto the next. An event's own value for the same key wins, and
the SDK logs one error for the collision.

## The person argument

Every call names who it is about, with at least one of:

| Field         | What it is                                                                       |
| ------------- | -------------------------------------------------------------------------------- |
| `userId`      | Your id for the signed-in user — the same one the browser passes to `identify()` |
| `anonymousId` | The browser's anonymous id, forwarded, for a visitor who has not signed in       |
| `sessionId`   | Optional. The browser's session id, forwarded, so the event joins that visit     |

An event with neither `userId` nor `anonymousId` is dropped with an error. Server events carry no
device, browser or location.

## `track(eventName, properties, person)`

```ts
analytics.track('order_paid', { plan: 'pro' }, { userId: user.id });
```

Returns the event's `eventId`, or `undefined` when nothing was queued — no person named, a refused
name, a malformed write key or one already rejected, after `shutdown()`, or a full queue. The key
is checked in the background, so a call made before a rejection comes back still returns an id, and
the rejection drops that event. An id means the event was queued, not that it arrived. A test
passes these ids to [`verify_tracking_by_event_ids`](/docs/reference/mcp-tools#verify_tracking_by_event_ids)
after `await analytics.flush()`.

## `setPersonProperties(ops, person)`

```ts
analytics.setPersonProperties({ set: { plan: 'pro' } }, { userId: user.id });
```

`set`, `setOnce` and `unset`, as in the browser. Takes `userId` or `anonymousId`, not `sessionId`.
Returns the profile write's `eventId`, or `undefined` when nothing was queued — an empty call, no
person named, a malformed or already-rejected write key, after `shutdown()`, or a full queue.

## `flush()`

`await analytics.flush()` sends everything queued now. **In a serverless function, call it before
you return**, because the platform may freeze the process as soon as the response is sent. A send
that fails on the network or with a server error is retried twice within about a second before
`flush()` resolves.

## `withFlush(handler)`

```ts
export const handler = analytics.withFlush(async (event) => {
  analytics.track('order_paid', { plan: 'pro' }, { userId: event.userId });
  return { statusCode: 200 };
});
```

Returns your handler wrapped so `flush()` runs after it finishes — also when it throws. What the
handler returns or throws is passed through unchanged. Use it for every serverless handler, so no
return path can skip the flush.

## `shutdown()`

`await analytics.shutdown()` sends what is queued and stops the client. Call it on `SIGTERM` in a
long-running server. A script that exits on its own sends its queue first anyway; `shutdown()` is
the explicit way to be sure.

## Delivery

Events are batched and retried with backoff when the endpoint is unreachable or busy, keeping the
same event id across attempts.
