Use the server SDK where the thing happened on your server — a payment confirmed, a subscription
renewed, an invite accepted. In browser code, use @togohq/sdk-browser.
Node 20 or later; zero runtime dependencies.
Install and create one client
npm install @togohq/sdk-node
import { init } from '@togohq/sdk-node';
// One client per process, shared by every request.
export const analytics = init({ writeKey: process.env.TOGO_WRITE_KEY! });
Use the test key (togo_pkt_…) in development and staging and the live key
(togo_pkl_…) in production. They are the same public write keys the browser uses.
Name the person on every call
A server handles many people at once, so the SDK remembers nobody. Every call says who it is about:
analytics.track('order_paid', { plan: 'pro', seats: 5 }, { userId: user.id });
userId— your id for the signed-in user. Use the same one the browser passes toidentify(), or each user becomes two people.anonymousId— for a visitor who has not signed in: the browser’s anonymous id, forwarded from the page (analytics.getAnonymousId()in the browser).sessionId— optional: the browser’s session id, forwarded, so the event joins the visit it belongs to.
An event with neither userId nor anonymousId is dropped with an error in your logs, because
nobody could be credited with it. The SDK never makes an id up.
Send events from your server for the right person shows how to
forward the browser’s ids.
Make sure events leave
Events are batched — 100 per request, or every 10 seconds — and retried with backoff when Togo is unreachable. How you end depends on where the code runs:
| Where | Do this |
|---|---|
| Serverless function (Vercel, Netlify, Lambda, Workers) | Wrap the handler in analytics.withFlush() |
| Long-running server | await analytics.shutdown() on SIGTERM |
| Script or job | Nothing — queued events are sent before the process exits |
Serverless is the case that loses events. The platform may freeze the function the moment the
response is sent, with the batch still in memory. withFlush() sends it before the handler
returns, on every return path and when it throws:
export const POST = analytics.withFlush(async (request: Request) => {
// ...
analytics.track('invite_accepted', {}, { userId });
return Response.json({ ok: true });
});
Or call await analytics.flush() yourself before every return.
process.on('SIGTERM', async () => {
await analytics.shutdown();
process.exit(0);
});
Nothing on the client throws. A problem is logged as [togo] … and the event is retried or
dropped. At most 10,000 events wait in memory while Togo is unreachable; past that, new events are
dropped with an error.
Profile properties
analytics.setPersonProperties(
{ set: { plan: 'pro' }, setOnce: { first_plan: 'free' } },
{ userId: user.id },
);
Set person properties explains which keys are kept.
Check it works
Run the code path with the test key and confirm the events arrived:
Check that events arrive. All options are in the
@togohq/sdk-node reference.