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
`401 auth/tracking-secret-key` on `client.track()`
`401 auth/tracking-secret-key` on `client.track()`
- Wrong environment — you used the dev secret in prod or vice versa. Most common.
- Typo or whitespace — copy-pasted with extra characters. Re-copy from the dashboard.
- Recently rotated — your service hasn’t reloaded the env var.
Authorization: Bearer ... header AND the sdkSecretKey body field both contain the same value, and that value is the current secret for your target environment.`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 toDev(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://eu.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.