Skip to main content

@notifizz/nodejs SDK reference

@notifizz/nodejs is the Node SDK for tracking events from a backend service. It exposes a single track() method, the enricher subsystem, the event catalog, and a hashed-token helper for widget auth. Java and PHP carry the same surface since their 2.0.0; what is specific to Node is the bundled Zod re-export.

TL;DR

  • new NotifizzClient(authSecretKey, sdkSecretKey, webhookSigningSecret?, clientOptions?) — the third argument is required only when you call dispatch() to expose enrichers; the fourth carries schemaMode, onError, baseUrl, discoveryUrl, webhookName.
  • await client.track(eventName, properties, options?) posts a single event to POST /v1/events/track with retries (1s, then 2s) 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 forwards req.body and returns the result.
  • import { z } from "@notifizz/nodejs" — re-exported Zod, the zero-config path. Zod 3 and Zod 4 schemas are both accepted, including ones built by your own Zod instance.
  • await client.ready() — call it once your server is listening, so a non-production environment re-runs discovery immediately instead of on the next periodic pass.
  • Errors live inside the dispatch response body ({ ok: false, error: { code, message } }) — your controller writes 200 unconditionally.

Installation

Constructor

NotifizzClientOptions

clientOptions is the fourth argument. Passing it third — where webhookSigningSecret goes — fails silently: the options are ignored, the client stays in 'soft' mode, and dispatch() later signs against an object instead of a secret and answers hmac-invalid. Pass undefined in third position when this service exposes no enricher.
Keep keys in environment variables. They are scoped per environment (dev / prod) — see keys and environments.

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

Behaviour

  • Posts { eventName, properties, sdkSecretKey, idempotencyKey } to POST /v1/events/track — plus occurredAt when you declare one.
  • Sends Authorization: Bearer <sdkSecretKey> and X-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 same idempotencyKey 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.

Event time (occurredAt)

By default, the moment Notifizz receives an event is the moment it happened — right, when you track at the point the thing occurs. Declare the event time when the two genuinely differ: replaying a queue, flushing a batch collected offline, backfilling history.
Accepts a Date or an ISO 8601 string. When you don’t pass it the field is left out of the request entirely — an absent field tells the API that reception time is the only truth. A date in the future is refused with event/invalid-occurred-at; a 60-second tolerance absorbs client clock skew. The value feeds time-scoped resolutions server-side, so it must never claim a state that does not exist yet. Available since 2.2.0. See the Event Tracking reference for the full wire format.

Declaring events

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.
For multiple events, just call 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.
Override per environment with the NOTIFIZZ_SCHEMA_MODE env var:

Discovery exposure

Declared events are exposed alongside enrichers through the same client.dispatch() webhook. On a kind: 'discovery' payload, dispatch returns the unified catalogue:
The Notifizz backend pulls this every 30 minutes (or when “Refresh” is clicked in the dashboard). Schemas using Zod are converted to JSON Schema 2019-09 at registration time via 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.
Returnsstring, hex-encoded SHA-256. See backend tokens for the widget side.

Audience identity

identify() links two Subjects to the same Audience — the mechanism that makes an application user and an email address the same person. detach() puts a Subject back into an Audience of its own.

What the action means

Links are always declared, never inferred: Notifizz merges two identities because your code said so, not because two payloads looked similar. In a production environment, an EmailSubject on a known disposable domain is refused (disposable_email_domain); outside production the call succeeds and the response carries a warnings entry instead. Neither call retries — unlike track(), a failure bubbles straight out.

Enrichers

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 your dispatch route serves traffic.
Zod 3 and Zod 4 both work, from your instance or ours. Schemas are detected structurally, never with instanceof — so a schema built by your application’s own Zod is accepted, whether or not it is the copy the SDK bundles. Importing z from @notifizz/nodejs remains the zero-config path; importing from zod directly is fine when you need to share schemas across packages.One version constraint: serializing a Zod 4 schema needs the zod/v4 engine, which ships in zod >= 3.25. On an older Zod, registration fails at boot with an explicit message rather than shipping an untyped catalog. Fields Zod cannot represent in JSON Schema (z.date(), for one) degrade to {} individually — they never sink the whole registration.

client.enrichProfileData(options)

Registers the system enricher that feeds the Audience profile panel in the dashboard. It differs from enricher() on three points: it takes the reserved name notifizz:profileResolver (hidden from the enricher catalog), its input is fixed to { id, email } — one user per call, no batch — and its output is free-form.
Whatever fields you return are rendered in the profile panel — there is no output schema to declare.

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:
The Notifizz backend POSTs an envelope { 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: Errors are encoded inside the body, never thrown across the dispatch boundary. The customer’s framework writes 200 unconditionally — Notifizz interprets 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.

client.ready()

Signals that this service’s dispatch endpoint is mounted and serving its current set of enrichers and declared events. Call it once, when your HTTP server is listening.
Why a dedicated call rather than the boot signal the constructor already sends: the constructor runs before your enricher() and declareEvent() registrations and before the server binds its port, so at that moment the discovery endpoint cannot answer truthfully yet. ready() fires at the one instant the catalog is actually servable. The payoff is the local loop. In a non-production environment, the signal asks Notifizz to re-run discovery immediately, so an enricher or event property you just added shows up without a dashboard refresh and without waiting for the periodic pass. Because your wiring re-runs on every dev restart, each save re-announces automatically. Set NOTIFIZZ_DEBUG=1 to log the outcome of the signal.
Cross-language: ready() exists in the Node and Java SDKs. The PHP SDK has no equivalent — see PHP: boot signal.

Package exports

Everything importable from @notifizz/nodejs, grouped by purpose — in real code you would merge these into a single import.

client.config(options)

Overrides client options after construction. Useful when a value is only known once the process is running — typically the public URL, available after the server has bound its port.
Changing the announced target re-sends the boot announcement in the background, so a late config() still reaches the right place. schemaMode and onError are not reconfigurable — they are read once, at construction, from NotifizzClientOptions, which is also the type-safe way to set the three options above.

Error handling

track() throws the underlying axios error after exhausting retries. Wrap it when you need a custom log line:
Enricher execution errors are encoded in the response body by 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:
The full error catalog (including HTTP status mappings) is in error catalogue.

FAQ

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.
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.
A Zod 4 schema that could not be serialized. The zod/v4 engine the SDK needs for v4 schemas exists only in zod >= 3.25; below that, registration now fails loudly at boot instead of publishing an empty schema. Upgrade Zod, or import z from @notifizz/nodejs. If a single field is the problem rather than the whole schema, check whether it is a shape Zod cannot express in JSON Schema (z.date() is the usual one) — those degrade to {} on their own and leave the rest typed.
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.
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.
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.