@notifizz/nodejs SDK reference
@notifizz/nodejs is the Node SDK for tracking events from a backend service. It exposes a single track() method, a hashed-token helper for widget auth, and the only enricher subsystem available across the official SDKs today.
TL;DR
new NotifizzClient(authSecretKey, sdkSecretKey, webhookSigningSecret?)— third argument is required only when you calldispatch()to expose enrichers.await client.track(eventName, properties, options?)posts a single event toPOST /v1/events/trackwith retries (1s, then2s) and an idempotency key.client.enricher(name, options)registers an enricher.client.declareEvent(name, options)declares an event in the catalog.await client.dispatch(body)is the body-only webhook entry point — your controller forwardsreq.bodyand returns the result.import { z } from "@notifizz/nodejs"— re-exported Zod, mandatory to avoid the “two Zod copies” hooks crash in Vite-bundled apps.- Errors live inside the dispatch response body (
{ ok: false, error: { code, message } }) — your controller writes 200 unconditionally.
Installation
Constructor
| Parameter | Type | Required | Description |
|---|---|---|---|
authSecretKey | string | yes | Used by generateHashedToken() for widget backend-token auth. |
sdkSecretKey | string | yes | Used as the Bearer token on the tracking API and posted in the body. |
webhookSigningSecret | string | no | Required when calling dispatch() — used to verify the HMAC on incoming enricher webhooks. If omitted, enricher() and declareEvent() still register locally, but dispatch() throws on the first invocation. |
client.track(eventName, properties, options?)
Emits a single event. Notifizz resolves campaigns by eventName and runs each campaign’s orchestrator server-side to build the recipient list — there is no client-side workflow or recipient targeting.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
eventName | string | — | Canonical event name. Must match the event you registered in the dashboard. |
properties | Record<string, any> | {} | Arbitrary key-value data passed to campaigns. |
options.idempotencyKey | string | auto-UUID | Caller-supplied dedupe key. Set this when the same logical emit may be retried. |
Behaviour
- Posts
{ eventName, properties, sdkSecretKey, idempotencyKey }toPOST /v1/events/track. - Sends
Authorization: Bearer <sdkSecretKey>andX-Idempotency-Key: <idempotencyKey>. - Retries transient failures twice (1s, then 2s). After three total attempts, the last error bubbles up.
- Returns
Promise<void>. The backend acks acceptance; delivery happens asynchronously.
Idempotency
A retried emit with the sameidempotencyKey produces the same outcome — the backend responds { duplicate: true, idempotencyKey } and does not re-enqueue the event. Pick a key that uniquely identifies the logical event (e.g. order-shipped:${orderId}), not the call site.
See the Event Tracking reference for the full wire format.
Declaring events (Node-only today)
Declaring an event registers it in a catalog that the orchestrator AI and the dashboard can read. The orchestrator receives the event’s schema, description, and idempotency fields in its context — no more guessing properties from observed payloads. Declaring is optional; tracking always works.client.declareEvent(name, options)
Registers a single event on the client. The signature mirrors client.enricher(name, options) — name first, options second. Call once per event, typically at boot from a central catalog file. Returns the canonical event name as a string-literal type — store it for refactor-safe track() calls.
| Argument | Type | Required | Description |
|---|---|---|---|
name | string | yes | Canonical event name. Same grammar as track() (domain.event_name). Returned verbatim as a literal-typed string. |
options.description | string | yes | Surfaced in the dashboard catalog and sent to the orchestrator AI as part of the campaign context. |
options.schema | ZodType | raw JSON Schema | no | Validates payloads in track(). Optional — without it, the event is in the catalog but unvalidated. |
options.idempotencyFields | string[] | no | Property names the SDK uses to derive a deterministic idempotency key when one isn’t supplied. Implements option C of the idempotency RFC. |
declareEvent once per event (typically grouped in a events.ts catalogue module). There is no bulk variant — the SDK favours one declaration per call so each event keeps its own literal-typed return value.
Validation modes (schemaMode)
schemaMode is client-side only. The server never blocks a track based on a declared schema; the strict mode is a local guard rail to catch bugs in dev/CI.
| Case | soft | strict |
|---|---|---|
| Event not declared | console.warn, push | console.error, no push, track() rejects, onError invoked |
| Payload not matching declared schema | console.warn, push byte-for-byte unchanged | console.error, no push, track() rejects, onError invoked |
| All good | silent push | silent push |
NOTIFIZZ_SCHEMA_MODE env var:
Discovery exposure
Declared events are exposed alongside enrichers through the sameclient.dispatch() webhook. On a kind: 'discovery' payload, dispatch returns the unified catalogue:
zod-to-json-schema.
Conflict signals
If two services declare the same event name with diverging schemas on the same environment, the backend keeps last-write-wins and surfaces an “inconsistent declared event” banner on the event in the dashboard catalog. Resolve by aligning the declarations across services.Concept page
See Events for the why, the catalog UI, and the modes’ tradeoffs at a higher level.client.generateHashedToken(userId)
Generates the SHA-256 HMAC of userId + authSecretKey. Pass it to your frontend so the Notification Center widget can authenticate in backendToken mode.
| Parameter | Type | Description |
|---|---|---|
userId | string | The user’s unique identifier — must match the userId you pass to the widget. |
string, hex-encoded SHA-256.
See backend tokens for the widget side.
Enrichers (Node-only today)
An enricher is a server-side function the Notifizz orchestrator calls to fetch live data at notification time. You register one per data source (fetchUser, fetchOrder, …), expose them on a public URL, and the backend calls them via HMAC-signed webhooks. Cached responses respect the policy you declare.
The full protocol is in enrichers protocol reference. The end-to-end tutorial is in the enrichers guide.
client.enricher(name, options)
Registers an enricher on this client instance. Call once per enricher, before mounting handler().
| Option | Type | Required | Description |
|---|---|---|---|
description | string | no | Surfaced in the discovery endpoint. |
input | ZodType | raw JSON Schema | yes | Validated on every webhook call; throws EnricherInputValidationError on mismatch. |
output | ZodType | raw JSON Schema | yes | Discovery only — Notifizz validates server-side. |
cache | false | { ttl: number | string } | no | false disables caching; { ttl: "1h" } caches in Notifizz’s Valkey. Default: 1h. |
handler | (params) => Promise<output> | yes | Must be idempotent — the backend may retry after a network failure. |
client.enrichers()
Returns the list of registered enrichers. Useful for diagnostics and tests.
client.dispatch(body)
The body-only webhook entry point. Your controller is a one-liner regardless of framework:
{ payload, signature } where payload is a JSON-encoded discriminated union ({ kind: 'discovery' } or { kind: 'execute', name, params, timestamp }). dispatch() verifies the HMAC over payload, rejects stale timestamps (anti-replay, ±5 min), and returns:
| Result body | When |
|---|---|
{ ok: true, result: <discovery payload> } | kind: 'discovery' succeeded. |
{ ok: true, result: <enricher return> } | kind: 'execute' ran the registered handler. |
{ ok: false, error: { code, message } } | Anything failed. See enrichers protocol reference for the full code table. |
error.code and translates it back to typed domain errors. That is what lets the controller be one line.
dispatch() throws synchronously on the first call if webhookSigningSecret was not provided to the constructor.
Bundled exports
client.config(options)
Overrides default options. Currently only baseUrl is configurable.
| Option | Default | Description |
|---|---|---|
baseUrl | https://eu.api.notifizz.com/v1 | Base URL for all SDK API calls. |
Error handling
track() throws the underlying axios error after exhausting retries. Wrap it when you need a custom log line:
dispatch() — see the result table above. The customer’s controller never has to throw or set a status code; it just returns whatever dispatch() produces.
If you call enricher logic yourself outside the dispatch flow (rare — only for unit tests of a single handler), the typed errors are still exported:
FAQ
Why is `webhookSigningSecret` not required by default?
Why is `webhookSigningSecret` not required by default?
Because most clients only ever call
track() and generateHashedToken() — they never expose enrichers. Forcing the third argument would burden every customer with a secret they don’t need. The constructor accepts it as optional, and dispatch() throws if it’s missing on the first invocation.My enricher works locally but `dispatch()` returns `{ ok: false, error: { code: 'hmac-invalid' } }` in production.
My enricher works locally but `dispatch()` returns `{ ok: false, error: { code: 'hmac-invalid' } }` in production.
The HMAC over the
payload string did not match. Check two things: (1) webhookSigningSecret matches the value configured in the dashboard for this environment; (2) clock skew — error.code === 'stale-timestamp' means the payload’s timestamp is outside the ±5 min window, NTP your enricher host. See enrichers protocol reference.I get a hooks crash from Vite when registering an enricher.
I get a hooks crash from Vite when registering an enricher.
Two copies of Zod loaded — one bundled by the SDK, one from your
node_modules. Always import z from @notifizz/nodejs. If you really must use your own Zod (e.g. for sharing schemas across packages), pass a raw JSON Schema object instead of a Zod schema to enricher().Should I generate the idempotency key myself?
Should I generate the idempotency key myself?
Yes when the same logical emit may be retried (queued jobs, cron, retry middleware). Use a deterministic key derived from your domain —
order-shipped:${orderId} is better than crypto.randomUUID(), which generates a new key per attempt and defeats the dedupe.Is `track()` blocking? My request handler latency went up.
Is `track()` blocking? My request handler latency went up.
track() is async and the backend acks acceptance fast — but the SDK retries failures with 1s + 2s delays, so a degraded backend can stall your request handler for up to ~3s. Either fire-and-log (track().catch(...) without await) or push the emit onto a queue you control.Can I customise the retry schedule?
Can I customise the retry schedule?
Not today. The schedule is
[1000, 2000] ms. If you need different behaviour, wrap track() in your own retry layer and use a deterministic idempotency key so retries dedupe at the backend.See also
Event Tracking reference
HTTP wire format, idempotency contract, error shapes.
Enrichers tutorial
End-to-end walkthrough — register, mount, debug an enricher.
Backend quickstart
Send your first event in under five minutes.
Notification Center widget
Display the notifications your events drive.