Troubleshooting
The cross-cutting “something’s broken, where do I look?” guide. Each entry is a symptom, the most likely causes, and the fix. This page is itself the FAQ — so it skips the per-page accordion convention and lists everything inline.TL;DR — first checks
- Logs first. Your application logs catch SDK errors before anything else.
- Delivery History second. The dashboard’s per-event view shows whether Notifizz received the event and what it did.
- Workflow trace third. Each instance’s trace shows step-by-step what ran.
- Cross-environment contamination is the #1 cause of “weird”. Check the SDK secret and Front API Key match the environment you think you’re hitting.
Event tracking
400 event/invalid-name
400 event/invalid-name
reason field has the specific violation. Most common: uppercase letters, special characters, or starting with a digit. Use lowercase + dots + underscores: domain.event_name. See events.{ duplicate: true } after a retry
{ duplicate: true } after a retry
idempotencyKey. Your code does nothing.track() succeeded but no campaign ran
track() succeeded but no campaign ran
- Campaign status —
Editingonly sometimes fires;Archivednever does. Promote toImplementation(dev) orLive(everywhere). - Event name mismatch — case-sensitive.
Order.Shipped≠order.shipped. - Environment mismatch — campaign is
Liveon prod but you’re firing from a dev SDK secret. The campaign exists in both views; the runtime gate is environment-scoped.
Same event arrives twice — why isn't it deduped?
Same event arrives twice — why isn't it deduped?
idempotencyKey), each call gets a new UUID and dedup doesn’t trigger. For retried emits, set a deterministic key derived from your domain (order-shipped:${orderId}).Campaigns
Campaign worked in Dev, fails on Live
Campaign worked in Dev, fails on Live
- Enricher URL — set per environment. Prod has its own dashboard config; copy from dev or set fresh.
- Connector source — Stripe / Segment / etc. sources are per-environment. Wire prod separately.
- Variables — organisation variables and environment variables differ. Compare values in the dashboard side-by-side.
Campaign edits not taking effect
Campaign edits not taking effect
Editing → Dev → Live. Just saving keeps it at Editing. The dashboard shows the current status prominently in the campaign header.If the status is correct but a previous version is still firing on in-flight events: that’s by design. In-flight workflow instances run against the version they started on; new traffic picks up the new version.Two campaigns on the same event — only one runs
Two campaigns on the same event — only one runs
- Status — both must be runnable for the current environment.
- Event binding — both must explicitly bind to the same event id, not just the same name.
- Filtering in the orchestrator — if both orchestrators have the same
if (!matches) return [], both can output empty.
Widget
isReady never becomes true
isReady never becomes true
- Browser console — the widget logs auth errors with detail.
state.errorCode— typed error from the widget.auth/invalid-token-signaturemeans the JWT didn’t verify;auth/no-public-key-registeredmeans the env doesn’t have a JWKS configured.state.hasError === true— fires on any auth failure path.
authType, expired Front API Key, env mismatch (using a dev apiKey against a prod-deployed widget).Bell renders, badge stays at 0
Bell renders, badge stays at 0
userIdmatches the recipient identifier the campaign emits. String comparison; no implicit coercion.- Campaign actually fired — Delivery History → filter by user.
- First-load state — a fresh user has no inbox record; the widget creates it lazily. Fire one event before opening the widget for the first time.
Vite hooks crash with useState is null
Vite hooks crash with useState is null
react to a single instance:Widget mounted twice in dev mode (React StrictMode)
Widget mounted twice in dev mode (React StrictMode)
cancelled flags and a readyFired ref — you should not see two bells, but you may see two ready callbacks fire briefly. Production builds run effects once.Multiple users on the same browser — previous user's notifications visible
Multiple users on the same browser — previous user's notifications visible
destroy() was not called on logout. Without it, the widget caches real-time state per session. Always destroy + re-mount on identity change.Auth modes
backendToken mode — widget shows state.hasError = true
backendToken mode — widget shows state.hasError = true
userId you passed to the widget doesn’t match the userId you passed to client.generateHashedToken. Even a string-vs-number difference breaks the hash. Compare both sources character-by-character.Firebase token expires after 1 hour, widget breaks
Firebase token expires after 1 hour, widget breaks
onIdTokenChanged:state.hasError = true on expiry; that’s your re-mount cue if you don’t subscribe to token changes.PubliclySignedJwt — kid not found
PubliclySignedJwt — kid not found
kid header references a key not in your JWKS endpoint’s response. Three causes:- Wrong JWKS URL in the dashboard.
- IDP rotated keys and the cached JWKS is stale (Notifizz refreshes every few minutes).
- Cross-environment — the JWT was issued by a different IDP environment than the one whose JWKS you configured.
Connector webhooks
401 on every Segment / Stripe / HubSpot / Intercom delivery
401 on every Segment / Stripe / HubSpot / Intercom delivery
verificationSecret in the dashboard doesn’t match the one configured on the provider. Re-copy from the provider; some providers regenerate the displayed secret each time you click “show”.Stripe signature passes locally but fails in production
Stripe signature passes locally but fails in production
Provider sent a test event but no campaign fired
Provider sent a test event but no campaign fired
- Verified? Delivery History → filter by source. A failed signature shows up as a 401 entry.
- Mapped? The connector’s event-mapper translates provider event types into Notifizz events. Check the dashboard’s mapping UI for unmapped types — campaigns can’t fire on an event that hasn’t been named.
Enrichers
hmac-invalid
hmac-invalid
webhookSigningSecretmismatch between dashboard and constructor.- The framework re-serialised the body before forwarding —
dispatch()signs and verifies the verbatimpayloadstring inside the envelope, so re-stringifying the parsed inner object would change the bytes. Forwardreq.bodyas-is. Modern body parsers (Expressexpress.json(), Fastify, NestJS, Hono) preserve the envelope as JSON and never touch the innerpayloadstring. If you wrote a custom pre-processing layer, drop it.
stale-timestamp
stale-timestamp
Enricher called every time, despite cache: { ttl: '1h' }
Enricher called every time, despite cache: { ttl: '1h' }
(orgId, enricherName, paramsHash). If your campaign passes per-call data into the enricher params (timestamp, random id), every call is a miss. Check what the orchestrator actually passes — usually you want stable IDs (userId, orderId), not request-time data.Enricher returns expected data but campaign uses old values
Enricher returns expected data but campaign uses old values
cache: false for this enricher.Region
Wrong API host — getting 401 / 404 across the board
Wrong API host — getting 401 / 404 across the board
https://api.notifizz.com/v1. If your environment is in a different region, the SDK must hit the correct hostname. Override via client.config({ baseUrl: "..." }) or in the constructor. Check the dashboard environment settings for the correct hostname.