# Quickstart: Server (Node) (https://www.getfounderhq.com/docs/analytics/getting-started/quickstart-server-node)

Send an event your server knows about, and see it on the contact in
FounderHQ. About 5 minutes.

<Callout title="Before you start">
You need a FounderHQ account and a secret key. In the app, open
**Sequences → Settings → Event API keys**, create a key for
**Backend**, and copy it. The full key is shown only once. Keep it on
your server, never in a browser or a mobile app.
</Callout>

Use the Node SDK for facts only your backend can prove: a signup that
passed validation, a plan change, a webhook you received. Use a client
SDK for what people do in the interface.

## 1. Install the package

```npm
npm install @founderhq/events-node
```

## 2. Create one client for your process

```ts
// lib/analytics.ts
import { FounderHqNode } from "@founderhq/events-node";

export const events = new FounderHqNode(process.env.FOUNDERHQ_SECRET_KEY!);
```

Put `FOUNDERHQ_SECRET_KEY=fhq_sk_XXXX` in your server environment. The
client refuses a publishable key, so a mix-up fails at startup rather
than in production.

## 3. Capture a server-truth event

Every server event names the contact it belongs to. Pass your own user
ID, an email, or a phone number — at least one is required.

```ts
events.capture({
  contact: { externalId: "user_8421", email: "jane@acme.com" },
  event: "subscription.upgraded",
  properties: { plan: "growth", mrr: 99 },
});
```

Use the same `externalId` you pass to `identify` in the browser or the
app. That is what keeps one contact instead of two.

Retrying a webhook? Pass a stable `idempotencyKey` so the same delivery
counts once.

```ts
events.capture({
  contact: { externalId: "user_8421" },
  event: "subscription.upgraded",
  properties: { plan: "growth" },
  idempotencyKey: "stripe_evt_1a2b3c",
});
```

## 4. Flush before the process ends

The client batches in the background. Serverless functions and scripts
exit too fast for that, so flush explicitly.

```ts
await events.flush();
```

Call `await events.shutdown()` when your server stops, to send what is
left and stop the timer.

## Verify

1. Run the code path that captures the event.
2. In FounderHQ, open **Sequences → Contacts** and open Jane.
3. You see `subscription.upgraded` with `plan: growth` on her timeline.
4. **Sequences → Events** shows the same event in the live stream.

## Troubleshoot

### The event never arrives

Pass an `onError` handler when you create the client. It reports every
rejected or dropped event with the reason.

```ts
export const events = new FounderHqNode(process.env.FOUNDERHQ_SECRET_KEY!, {
  onError: (error, dropped) => console.error(error, dropped),
});
```

### A second contact appeared for the same person

Your server and your client are using different IDs. Both must use the
same stable user ID. See
[Identity and contacts](/analytics/concepts/identity-and-contacts).

### An event starting with `$` is ignored

Names that start with `$` are reserved for the SDKs. Send your own
name instead. Revenue has its own API — see [Revenue](/analytics/revenue).

## Next

- [Node SDK reference](/analytics/sdks/node)
- [Accounts and groups](/analytics/concepts/accounts-and-groups)
