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 takingwebhookSigningSecretand a 4-arg form takingNotifizzClientOptions.client.track(eventName, properties)— emits one event; throwsIOExceptionon 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 (Instantor ISO 8601String).- Each
track()retries twice (1s, then 2s) before bubblingIOException. client.enricher(name, options),client.declareEvent(name, options)andclient.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: onApplicationReadyEvent), 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()andsetMeasurementConsent()throwNotifizzApiException— anIOExceptioncarryinggetStatus()andgetError()— when the API refuses the call, since 3.0.0.NotifizzClientimplementsCloseable—close()releases the pooled HTTP connections.
Installation
- Maven
- Gradle (Kotlin DSL)
- Gradle (Groovy)
Constructor
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 }toPOST /v1/events/track— plusoccurredAtwhen you declare one. - Sends
Authorization: Bearer <sdkSecretKey>andX-Idempotency-Key: <idempotencyKey>. - Retries transient failures twice (1s, then 2s) — three total attempts.
- Throws
java.io.IOExceptionif 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.
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 sameMap.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:
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.
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.
Measurement consent
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.
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.
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.
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). InstantiateNotifizzClient 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:
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 addssetMeasurementConsent(). 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
Why does track() throw IOException?
Why does track() throw IOException?
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.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, 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.Can I host an enricher from my Java service?
Can I host an enricher from my Java service?
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.Is track() blocking?
Is track() blocking?
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.How do I configure a custom base URL for staging?
How do I configure a custom base URL for staging?
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.Can I batch several events in one call?
Can I batch several events in one call?
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.