Skip to main content

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

The SDK secret you passed doesn’t resolve to an environment. Three suspects:
  1. Wrong environment — you used the dev secret in prod or vice versa. Most common.
  2. Typo or whitespace — copy-pasted with extra characters. Re-copy from the dashboard.
  3. Recently rotated — your service hasn’t reloaded the env var.
Check: the Authorization: Bearer ... header AND the sdkSecretKey body field both contain the same value, and that value is the current secret for your target environment.
The event name failed the grammar. Body’s 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.
Not an error — it’s the success path for an idempotent retry. The backend has already processed this event under the same idempotencyKey. Your code does nothing.
The event reached Notifizz but no campaign matched. Three suspects:
  1. Campaign statusEditing only sometimes fires; Archived never does. Promote to Dev (dev) or Live (everywhere).
  2. Event name mismatch — case-sensitive. Order.Shippedorder.shipped.
  3. Environment mismatch — campaign is Live on prod but you’re firing from a dev SDK secret. The campaign exists in both views; the runtime gate is environment-scoped.
Check Delivery History → filter by event name → if zero entries, no campaign listened.
Idempotency keys are caller-supplied. If you let the SDK auto-generate (omitted 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

Almost always: prod is missing a dependency dev had.
  • 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.
Less common: prod traffic has a property shape your dev test events didn’t reproduce. The workflow trace pinpoints the missing field.
Did you save and promote? Status flow: 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.
Possible but unusual. Each match creates an independent workflow instance — they don’t gate each other. If you only see one running, check both campaigns:
  • 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

Authentication failed. Three layers to check:
  1. Browser console — the widget logs auth errors with detail.
  2. state.errorCode — typed error from the widget. auth/invalid-token-signature means the JWT didn’t verify; auth/no-public-key-registered means the env doesn’t have a JWKS configured.
  3. state.hasError === true — fires on any auth failure path.
Common causes: stale token, wrong authType, expired Front API Key, env mismatch (using a dev apiKey against a prod-deployed widget).
Auth succeeded but no message reached this user. Three checks:
  1. userId matches the recipient identifier the campaign emits. String comparison; no implicit coercion.
  2. Campaign actually fired — Delivery History → filter by user.
  3. 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.
Two copies of React in the bundle. The Notifizz widget script and your app are loading two distinct React copies. The fix is in your bundler config — alias react to a single instance:
// vite.config.js
resolve: {
  dedupe: ['react', 'react-dom'],
},
See the React SDK FAQ for the React-specific variant of this issue.
React 18+ runs effects twice in dev intentionally. The widget guards against duplicate mounts via 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.
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

Most common: the 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 ID tokens expire. Re-mount the widget on onIdTokenChanged:
onIdTokenChanged(getAuth(), async (user) => {
  if (user) setNotifizzToken(await user.getIdToken());
});
The widget surfaces state.hasError = true on expiry; that’s your re-mount cue if you don’t subscribe to token changes.
The JWT’s kid header references a key not in your JWKS endpoint’s response. Three causes:
  1. Wrong JWKS URL in the dashboard.
  2. IDP rotated keys and the cached JWKS is stale (Notifizz refreshes every few minutes).
  3. Cross-environment — the JWT was issued by a different IDP environment than the one whose JWKS you configured.

Connector webhooks

The signature didn’t verify. The most common cause: the 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”.
Clock drift. Stripe signatures include a timestamp; the verifier rejects requests outside its tolerance. NTP your Notifizz region (rare — Stripe’s tolerance is generous). Less commonly, the prod environment uses a different Stripe webhook than dev — re-copy the secret per environment.
Two layers to check:
  1. Verified? Delivery History → filter by source. A failed signature shows up as a 401 entry.
  2. 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

Two causes (in order of frequency):
  1. webhookSigningSecret mismatch between dashboard and constructor.
  2. The framework re-serialised the body before forwardingdispatch() signs and verifies the verbatim payload string inside the envelope, so re-stringifying the parsed inner object would change the bytes. Forward req.body as-is. Modern body parsers (Express express.json(), Fastify, NestJS, Hono) preserve the envelope as JSON and never touch the inner payload string. If you wrote a custom pre-processing layer, drop it.
Clock drift > 5 minutes. NTP your enricher host. Containers with broken time sync are a common cause.
The cache key is (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.
The cache served a stale entry. Your TTL is too long for the data freshness this campaign needs. Lower the TTL or set cache: false for this enricher.

Region

The default is 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.

See also

Observability

Correlation IDs, workflow traces, AI usage.

Error catalogue

Every typed error code.

Delivery history & stats

Aggregated view per campaign.

Keys and environments

Where each secret comes from.