Skip to main content

Notifizz Java SDK reference

com.notifizz:notifizz-java is the JVM SDK for tracking events. It exposes track() overloads, the enricher subsystem, the event catalog, a hashed-token helper for widget auth, and works the same from Java or Kotlin. The package is published to Maven Central.

TL;DR

  • new NotifizzClient(authSecretKey, sdkSecretKey) — plus a 3-arg form taking webhookSigningSecret and a 4-arg form taking NotifizzClientOptions.
  • client.track(eventName, properties) — emits one event; throws IOException on transient failures after retries.
  • client.track(eventName, properties, idempotencyKey) — overload for retried jobs.
  • client.track(eventName, properties, idempotencyKey, occurredAt) — overload declaring when the business event happened (Instant or ISO 8601 String).
  • Each track() retries twice (1s, then 2s) before bubbling IOException.
  • client.enricher(name, options), client.declareEvent(name, options) and client.dispatch(body) give the JVM the same enricher and event-catalog surface as the Node SDK, since 2.0.0.
  • client.ready() — call it once your server is listening (Spring: on ApplicationReadyEvent), so a non-production environment re-runs discovery immediately instead of on the next periodic pass.
  • client.setMeasurementConsent(params) passes on a person’s consent to the measurement of their email opens and clicks, or its withdrawal — since 3.0.0. Coming from 2.x: see upgrading from version 2.
  • identify(), detach() and setMeasurementConsent() throw NotifizzApiException — an IOException carrying getStatus() and getError() — when the API refuses the call, since 3.0.0.
  • NotifizzClient implements Closeable — close() releases the pooled HTTP connections.

Installation

No custom repository or credentials are required — Maven Central is used by default.

Constructor

Two more forms exist. Add the signing secret when this service exposes enrichers, and NotifizzClientOptions when you want a schema mode or a rejection callback:

client.track(eventName, properties)

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.

client.track(eventName, properties, idempotencyKey)

Overload with an explicit idempotency key. Use this when the same logical emit may run twice (retry, dedupe).

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) — three total attempts.
  • Throws java.io.IOException if all attempts fail or the response status is >= 400.

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.
Both overloads leave the field out of the request when you pass null — 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.

Kotlin

The SDK works naturally with Kotlin — the same Map.of / mapOf interop applies:

client.generateHashedToken(userId)

Generates the SHA-256 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.

client.config(opts)

Overrides default options. Currently only baseUrl is configurable.

Declaring events

Declaring an event registers it in a catalog the orchestrator AI and the dashboard can read — schema, description and idempotency fields included, instead of properties guessed from observed payloads. Declaring is optional; tracking always works without it. Available since 2.0.0.
declareEvent() returns the canonical (trimmed) event name — store it and pass it to track() so a rename stays a one-line change. client.declaredEvents() returns the discovery view for diagnostics and tests. Schemas are plain JSON Schema (Map<String, Object>). JsonSchema is a fluent builder for the common shapes; hand-built maps work just as well.

Validation modes (SchemaMode)

The schema mode is SDK-local. The Notifizz server never blocks a track over a declared schema — strict mode is a guard rail for dev and CI. Set it per environment without touching code:
When idempotencyFields is declared and you pass no key, track() derives a deterministic one from those properties — a retry of the same logical event dedupes on its own. See Events for the catalog UI and the tradeoffs at a higher level.

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, expose them on a public URL, and the backend calls them via HMAC-signed webhooks. Available on the JVM since 2.0.0. The full protocol is in the enrichers protocol reference.

client.enricher(name, options)

Registers an enricher on this client. Call once per enricher, before your dispatch route serves traffic.
client.enrichers() returns the discovery view of what is registered — handy in tests.

client.enrichProfileData(handler)

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 schema to declare on the way out.

client.dispatch(body)

The body-only webhook entry point. Your controller is a one-liner:
dispatch() accepts the parsed body (Map) or a raw JSON String. It verifies the HMAC over the inner payload, rejects stale timestamps (anti-replay, ±5 min), and routes discovery versus enricher execution. Errors are encoded inside the body, never thrown across the dispatch boundary — your framework writes 200 unconditionally and Notifizz translates error.code back into a typed domain error. That is what keeps the controller one line. dispatch() throws IllegalStateException if no webhookSigningSecret was passed to the constructor. NotifizzClient.signDispatchPayload(secret, payload) produces the same HMAC, for testing a dispatch route with synthetic signed requests.

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. Available since 2.0.0.

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. Two kinds of link are accepted: a user id with an email address — the address joins that person — and two user ids, where the second joins the first. An address that another of your users already holds is never linked to a second one (identity_conflict, final): an address belongs to one user id at a time, and merging would expose one person’s history to the other. The user id you declared is still recorded — without the address. Two email addresses can’t be linked to each other (identify_requires_a_client_id): link each of them to the user id they belong to. An address must be valid; surrounding spaces are ignored (invalid_subject_identifier otherwise). A link does not expire — call detach() to undo it. 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. A 409 with identity_concurrent_update means another call changed the same identities at the same moment: nothing was linked, and the call is safe to repeat. The calls act in the environment of your SDK secret key: a missing or unknown key answers 401 (invalid_sdk_key), an environmentId that isn’t the key’s environment answers 403 (environment_mismatch). Every refusal names its code in the error field of the response body. Both calls throw NotifizzApiException when the API answers with a status outside 2xx: the call was refused, and nothing was linked. It extends IOException, so the throws IOException of these methods is unchanged; getStatus() is the HTTP status and getError() the refusal code (identity_conflict, environment_mismatch, …), or null when the response carries none. A network failure throws a plain IOException. Neither call retries. setMeasurementConsent() tells Notifizz whether a person consents to the measurement of their email opens and clicks, or withdraws that consent. Call it once, when the answer is given or changes — not on every send: it is a lasting property of the person, not of an event. What it changes on each email is described in open and click measurement. Available since 3.0.0.
  • A refusal always wins — over your organisation’s declaration, over a consent found on another of the person’s records, and on emails already sent.
  • A consent is never lent. Passed on for an email address, it counts for a person only if you linked that address to them with identify(), or if they are the only person among your users who holds it. detach() clears every consent on the records of both sides (refusals are kept): pass the consents on again under each user id. See consent person by person.
  • A consent is what Only those who consented measures. Under All my recipients have consented, everyone who has not refused is measured.
  • Until your organisation has declared, nobody is measured: the answer is stored, and applies from the declaration on.
  • The environment of the key. The answer reaches every record of the person in that environment — by address, by user id, and the ones you linked with identify(). With a non-production key, the call is accepted and has no effect: only production emails reach real recipients.
A refusal — nothing is written — throws NotifizzApiException, with the HTTP status in getStatus() and the refusal code in getError(): 401 invalid_sdk_key, 403 environment_mismatch, 422 unsupported_subject_type, 422 invalid_subject_identifier (an empty identifier, or an email address that is not valid), 422 invalid_consented (consented is not a boolean). getError() is null when the response carries no code, a 5xx answered by a proxy for instance. A network failure throws a plain IOException. The call is idempotent, and the SDK does not retry it: on a network error or a 5xx, call it again until it succeeds — a withdrawal that never reaches Notifizz is not honoured.
Call it from your backend only — it carries your SDK secret key.

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 — in Spring Boot, from an ApplicationReadyEvent listener.
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.
Cross-language: ready() exists in the Node and Java SDKs. The PHP SDK has no equivalent — see PHP: boot signal.

Lifecycle

All calls share one pooled HTTP client with bounded timeouts — connect 2 s, response 5 s, pool checkout 2 s (since 2.1.0). Instantiate NotifizzClient once and keep it: a client per request throws the pool away every time and pays a fresh TLS handshake. NotifizzClient implements Closeable; close() releases the pool. A long-lived singleton can skip it — the JVM reclaims the connections at exit — but a managed bean should not: Spring picks close() up as the destroy method automatically.

Error handling

track() declares throws IOException. Wrap it where you need a custom log line:
After three failed attempts, the last IOException is rethrown — the SDK does not silently swallow failures. The full error catalogue (including HTTP status mappings) is in error catalogue.

Upgrading from version 2

Version 3.0.0 makes the identity calls throw on a refusal, removes calls that never worked and adds setMeasurementConsent(). Bump the dependency to 3.0.0 (see Installation), then: Nothing else changes: track(), enrichers, declared events and widget tokens behave as in 2.x.

FAQ

Tracking is a network call to POST /v1/events/track. After the SDK exhausts its 3 retry attempts, the underlying IOException (or a wrapped one for >= 400 HTTP statuses) is rethrown. Always handle it — silent network failures are a foot-gun.
Yes when the same logical emit may be retried (queued jobs, scheduler, retry middleware). Use a deterministic key derived from your domain — order-shipped:{orderId} is better than UUID.randomUUID() from outside the SDK, which generates a new key per attempt and defeats the dedupe.
Yes, since 2.0.0. Register handlers with client.enricher(name, options), pass a webhookSigningSecret to the constructor, and expose client.dispatch(body) on a public route — see Enrichers above. No Node service in the middle.
Yes — track() is synchronous. Since 2.1.0 the wait is bounded: three attempts, each capped at 2 s to connect and 5 s for the response, plus the 1 s + 2 s backoff between them. A fully degraded backend can therefore stall the calling thread for roughly 24 s. Run it on an ExecutorService or a virtual thread when latency matters — and never inside a database transaction, where the wait would hold a connection hostage.
Call client.config(Map.of("baseUrl", "...")) after construction. The SDK reads the option on every track() call. Use this for local mocks, regional endpoints, or staging environments — production is https://api.notifizz.com/v1.
Not today. Each track() call is one event. Fire them in parallel with an executor when batching matters; idempotency keys ensure retries dedupe at the backend.

See also

Event Tracking reference

HTTP wire format, idempotency contract, error shapes.

Backend quickstart

Send your first event in under five minutes.

Event Tracking overview

Cross-language feature matrix.

Notification Center widget

Display the notifications your events drive.