@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 calldispatch()to expose enrichers; the fourth carriesschemaMode,onError,baseUrl,discoveryUrl,webhookName.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, 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
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 }toPOST /v1/events/track— plusoccurredAtwhen you declare one. - 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.
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.
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 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.
Returns —
string, 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:
{ 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.
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:
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.My enricher shows up in the catalog with no types — everything is untyped.
My enricher shows up in the catalog with no types — everything is untyped.
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.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.