# Node.js and server SDK (@metricraven/node)

> Send goals, identify, payments, refunds and crawler hits from Node, Bun, Deno, Cloudflare Workers or Vercel Edge with typed calls.

Source: https://metricraven.com/docs/node-sdk  
Markdown: https://metricraven.com/docs/node-sdk.md  
Section: Installation guides

`@metricraven/node` sends what only your backend knows: confirmed sign-ups, verified identities, payments and refunds. It runs anywhere `fetch` does, including Node 18+, Bun, Deno, Cloudflare Workers and Vercel Edge, and it has no dependencies.

It wraps the same secret-key endpoints as [server events](https://metricraven.com/docs/server-events), [identify](https://metricraven.com/docs/identify-users) and the [Payments API](https://metricraven.com/docs/payments-api), with types, timeouts and retries. Calls never throw into your request handler: each one returns a result.

## Install

1. **Add the package**

   ```bash
   npm install @metricraven/node
   ```

2. **Set your secret key on the server**

   Copy the site secret key (`ana_sec_…`) from Settings and store it as `METRICRAVEN_SECRET_KEY`. Never ship it to the browser or a mobile app.

3. **Send an event after the action succeeds**

   ```typescript
   // lib/metricraven.ts — server only. Reads METRICRAVEN_SECRET_KEY.
   import { createMetricRaven, getVisitorId } from "@metricraven/node";

   export const metricraven = createMetricRaven();

   // In your sign-up route handler, after the account exists:
   await metricraven.track({
     name: "signup",
     userId: user.id,
     visitorId: getVisitorId(request), // the tracker cookie, or null
     props: { plan: "free" },
   });
   ```

> **Verify it works**
>
> The event appears in the **Goal** tab at the bottom of Traffic after refreshing. If it doesn't, log `result.error` and check it against the table under **Results and errors** below.

## Calls

| Call | What it sends |
| --- | --- |
| track({ name, userId?, visitorId?, props?, timestamp? }) | A goal (server event). Needs userId or visitorId. Up to 10 props. |
| identify({ visitorId, userId, email?, name?, traits? }) | A verified link from a visitor to your user. It replaces any identity the browser claimed. |
| payment({ amount, currency, transactionId, visitorId?, userId?, plan?, customerId?, renewal?, isFreeTrial?, email? }) | A payment in major currency units. Repeats of a transactionId are ignored. Without visitorId or userId, email matches a visitor identified with it or a browser that called payment({ email }); see Attribute by email. |
| refund({ amount, currency, transactionId }) | A refund, with the cumulative amount refunded so far. |
| crawl({ url, userAgent, status?, ip? }) | One crawler request. Uses the bot token when one is set. For middleware, use @metricraven/ai-crawl. |

```typescript title="Identify"
await metricraven.identify({
  visitorId,
  userId: user.id,
  email: user.email,
  name: user.name,
  traits: { plan: "pro" },
});
```

```typescript title="Payments and refunds"
// After the payment succeeds. amount is in major units: 29 means $29.
await metricraven.payment({
  amount: 29,
  currency: "USD",
  transactionId: order.id,
  visitorId,
  userId: user.id,
  plan: "pro",
});

// Refunds send the cumulative amount refunded so far.
await metricraven.refund({ amount: 15, currency: "USD", transactionId: order.id });
```

`track`, `identify`, `payment`, `refund` and `crawl` are also exported as functions that use a shared client configured from the environment, or from `configure(options)`.

## Bot token for crawler reports

If a server only reports crawlers, give it the site's ingest-only [bot token](https://metricraven.com/docs/bot-traffic-settings#bot-token) (`mrbot_…`) as `METRICRAVEN_BOT_TOKEN` or `botToken`, instead of the secret key. It can only send crawl events.

| Configured | What happens |
| --- | --- |
| Bot token only | crawl() uses it. track, identify, payment and refund return bot_token_cannot_track (…_identify, …_payment, …_refund) without a request. |
| Bot token and secret key | crawl() uses the bot token; every other call uses the secret key. |
| siteId (METRICRAVEN_SITE_ID) | Optional. Crawl reports are refused (wrong_site) if the token belongs to another website. |

## Attribute to the visit

`getVisitorId(request)` reads the visitor ID from a Fetch `Request`, a Node or Express request, `Headers` or a raw `Cookie` header. It checks the tracker's `ana_vid` cookie, then the `metricraven_visitor_id` cookie that the checkout recipes set. It returns `null` when there's neither.

> **Note:** The tracker only stores its cookie after the visitor gives storage consent. Without consent, send `window.analytics.getVisitorId()` from the browser with your own request, or rely on `userId` after identify.

## Results and errors

```typescript
const result = await metricraven.track({ name: "signup", userId: user.id });
if (!result.ok) console.warn("MetricRaven:", result.error, result.status);
```

| error | Meaning |
| --- | --- |
| invalid_input | Rejected before sending, for example a bad event name, visitor ID or currency. |
| missing_secret_key / invalid_secret_key | No key, or not an ana_sec_ key. Nothing was sent. |
| invalid_bot_token | botToken isn't an mrbot_ token. Nothing was sent. |
| bot_token_cannot_track (…_identify, …_payment, …_refund) | Only a bot token is set, and it can only report crawlers. Nothing was sent. |
| unauthorized | The key wasn't accepted, for example after a rotation's grace period ended. |
| invalid_payload | The API rejected the body. |
| domain_not_allowed | The crawl URL's host isn't one of the site's domains. |
| tracking_paused | The plan's usage limit was reached. |
| bot_tracking_paused | crawl: the bot request allowance is used up until the reset. |
| bot_token_required | crawl: the website only accepts crawler reports sent with its bot token. |
| wrong_site | crawl: the bot token belongs to a different website than siteId. |
| timeout / network_error / http_5xx | No usable response after retries. |

## Options

| Option | What it does |
| --- | --- |
| secretKey | Defaults to METRICRAVEN_SECRET_KEY. |
| botToken | Defaults to METRICRAVEN_BOT_TOKEN. Used by crawl() only. |
| siteId | Defaults to METRICRAVEN_SITE_ID. Optional check of the bot token's website. |
| apiHost | Defaults to METRICRAVEN_API_BASE, then https://metricraven.com. |
| timeoutMs | Per attempt. Defaults to 5000. |
| retries | Extra attempts after 5xx or network errors. Defaults to 2. 4xx responses are never retried. |
| retryDelayMs | First backoff wait. Defaults to 300, and doubles each time with jitter. |
| onError | Called with each failure: code, status and endpoint. Never includes the key or token. |
| throwOnError | Rejects instead of returning { ok: false }. |
| disabled | Skips the network, for tests and local development. |
| fetch | A custom fetch. |

## Edge and serverless

Don't hold the response while a call runs. Hand the promise to `waitUntil` (Workers `ctx`, Next.js `after()` or Vercel's `waitUntil`):

```typescript
// Cloudflare Workers: don't hold the response for analytics.
import { createMetricRaven } from "@metricraven/node";

export default {
  async fetch(request, env, ctx) {
    const metricraven = createMetricRaven({ secretKey: env.METRICRAVEN_SECRET_KEY });
    ctx.waitUntil(metricraven.track({ name: "api_call", userId: "user_42" }));
    return new Response("ok");
  },
};
```

## Details

- After a timeout, payments and identify are retried, because the server deduplicates or overwrites them. Events and crawls aren't retried after a timeout, because they have no deduplication key.
- The unscoped `metricraven` package exposes the same API at `metricraven/node`.
- ESM and CommonJS, with TypeScript types. It uses only `fetch`, with no Node built-ins.
