Skip to content

Setup guide for coding agents

Last updated

This guide is for the coding agent that installs Whisperr in an app, and for the developer who reviews the change. It works in any agent: Claude Code, Cursor, Windsurf, Codex, Replit, Lovable, Bolt, Rork and others. Follow it from top to bottom. The plain-text version is https://docs.whisperr.net/setup.md.

When you finish, the app sends who each user is (identify) and what they do (track) to Whisperr, and the user sees those events arrive in the Whisperr dashboard.

The Whisperr dashboard gives the user a short prompt to paste into the agent. It starts with the first line below. Each other line is there only when Whisperr knows the value. For example:

Install Whisperr in this app: follow https://docs.whisperr.net/setup.md
Whisperr publishable key: wpk_…
Platform: Expo
Email permission: Per user
Main action in this app: A user logs a workout
Events already registered: account_created, workout_logged
LineWhat to do with it
Whisperr publishable keyThe app key (wpk_…). Put it in a public environment variable (step 3).
PlatformWeb, Next.js, React, Expo, React Native, Flutter or Swift. Check it against the repository (step 1).
Email permissionThe user’s answer to Does your signup get permission to email users?: Yes, Per user, No or Not answered. It decides what identify sends (step 5).
Main action in this appThe moment a user gets value from the app. Track it as the core event (step 6).
Events already registeredEvent codes that Whisperr already uses for this app. Track these codes exactly as written.

No key in the prompt? Ask the user to create one. In the dashboard, on Home → Connect your code → Copy prompt, Add an app key puts a new key in the prompt (workspace owners and admins). Or the user creates one in Settings → API keys with the kind App. Every workspace can create keys, also before the trial starts.

The server gives you this app’s own event plan, the exact SDK guide for the platform, and the events that really arrive. Then follow the playbook it returns. The rules in this guide still apply.

Check for a connected server first. Look for an MCP server named whisperr, or one with the address https://mcp.whisperr.net/install or https://mcp.whisperr.net/mcp (in Claude Code: claude mcp list). The dashboard’s coding-agent setup and the Whisperr plugin for Claude Code or Codex add one. When it is there, use it and do not add a second one, also when it says it needs authentication:

  1. When it needs sign-in, start the sign-in yourself. In Claude Code, call the server’s authenticate tool and show the user the sign-in URL it returns. If there is no such tool, ask the user to run /mcp, select whisperr and choose Authenticate, and end your turn so they can. In other agents, ask the user to sign in where the agent lists its MCP servers. A sign-in that is still open is not a failure.
  2. After you show the URL in Claude Code, do not end your turn to wait for a reply. When the user approves in the browser, Claude Code connects the server by itself, and with tool search (the default) the Whisperr tools appear in the same turn. Check for them about every 15 seconds for up to 5 minutes, and continue as soon as they are there. Between checks, only read the repository to find the platform and the app’s entry point; change no files yet. If the tools are still missing after 5 minutes, ask the user whether they finished signing in, and keep waiting while they are still signing in. In other agents, and after /mcp, wait until the user says they finished and the Whisperr tools work.
  3. Use the install without MCP only when the user declines the sign-in or it cannot finish. Then tell the user that you install without MCP, and why.
  4. Pass the prompt’s publishable key (wpk_…) as publishable_key to the install tools, starting with get_required_events. When the connection is signed in to another workspace than the key’s, the tools refuse with workspace_mismatch and say how to sign in again. In Claude Code: run /mcp, select whisperr, choose Clear authentication, then Authenticate and sign in to the account and app that the key belongs to. Never register events into another workspace.

The sign-in opens in the browser, where the user picks the Whisperr app and approves the scopes. Registering events needs a workspace owner or admin. Access tokens last 15 minutes, and the client renews them with the refresh token without a new sign-in. See How long does the sign-in last?

Skip this when a Whisperr server is already connected (see above).

The address for installs is https://mcp.whisperr.net/install. It has only the install tools. https://mcp.whisperr.net/mcp has every Whisperr tool, also data and dashboard questions. Both use Streamable HTTP and OAuth 2.1.

claude mcp add --transport http whisperr https://mcp.whisperr.net/install

Then run /mcp, select whisperr and choose Authenticate, and wait until the sign-in finishes.

A connection is signed in to one Whisperr workspace. A client that already signed in for another app reuses that sign-in. When get_workspace_context names another app than the one you install, stop before you register anything, and ask the user to sign in again:

  1. Run /mcp and select whisperr.
  2. Choose Clear authentication.
  3. Run /mcp again, select whisperr, and choose Authenticate.
  4. In the browser, sign in to the Whisperr account of this app. On the consent screen, select this app under Connect it to, then choose Allow access.
  5. Call get_workspace_context and check that it names this app.

Do not use Re-authenticate to switch workspaces. It can keep the previous workspace’s sign-in, even after you approve the new app.

With the server connected, call the tools in this order:

  1. get_required_events: the compact work order. It lists the events to wire with priority, slot and side, the install steps, and the playbook’s event guides (slot, title, priority, and a status once Whisperr’s setup has a plan).
  2. Map the whole app: every screen or route, server handler, background job and webhook. Read the router, the data models and the server handlers. Note the core loop and where money changes hands. This map is the event inventory.
  3. get_event_guide, once, with slots (the ones you will wire, the guides you cannot answer from the title, and the nice guides that fit this app) and codes (work-order events whose description or properties you need). It returns when and where each event fires and the property names Whisperr reads.
  4. Wire every work-order event and guide moment that the app really has, without asking. Answer every must and should guide: present, with the file where it happens, or absent, with a one-line reason. A guide with status found is already covered. Never wire one with status from_billing_source. See Track events for the base events, money moments and extra events.
  5. get_integration_playbook with the platform (web, react, nextjs, react_native, flutter, swift, node, python, php or dotnet): the package, install command, key placement, init and calls.
  6. Edit the code. Then register every new event code you wired in one draft: prepare_code_source, create_registration_draft, begin_registration_validation, then submit_isolated_test_events with one test event per event: one call for up to 50 events, otherwise batches of 50. Each test sends the property names that the code’s call site sends, with made-up values. Never add a property only to pass. missing_properties on a result names registered properties that the test did not send. Then commit_registration: when its declared_not_sent lists a property that the code does not have, drop it from the schema in the next registration. Never invent a value for it. Put install_coverage in your summary. When you registered nothing, read get_install_coverage instead. On a signed-in connection, a later commit_registration adds events to the ones already committed.
  7. get_received_events: check that real events arrive while the user uses the app. You do not run the app yourself (step 8).
  8. get_setup_status: tell the user the setup score and the next step.

Whisperr tools never return API keys. Ask the user for the key as described above. More about the server and the agent kit: Install with your coding agent.

If your agent cannot add an MCP server, or the user declines the sign-in or it cannot finish, use the install below and tell the user so. It needs no MCP.

Check the Platform line against the repository. The repository wins when they disagree. Check the rows in order and use the first that matches: an Expo app also has react and react-dom, and the web SDKs do nothing in a native app.

The repository hasPlatformPackages
package.json with expoExpo@whisperr/react-native, and @whisperr/expo for push
package.json with react-native, without expoReact Native@whisperr/react-native
package.json with next, without expo or react-nativeNext.js@whisperr/next, @whisperr/web
package.json with react and react-dom, without next, expo or react-nativeReact@whisperr/react, @whisperr/web
Another front-end package.json (Vite, Vue, Svelte, Angular, plain), without expo or react-nativeWeb@whisperr/web
pubspec.yaml with the Flutter SDKFlutterwhisperr, and whisperr_firebase_messaging for push with firebase_messaging
Package.swift, *.xcodeproj or *.xcworkspace, without React Native or FlutterSwiftWhisperr (Swift package)
A backend in Node.js, Python, PHP or .NETServer@whisperr/node, whisperr (PyPI), whisperr/php, Whisperr (NuGet)

Install a client SDK in the app. When the repository also has a backend that knows about payments or email consent, add the server SDK there. Use the same user id in both, so the events land on one timeline.

Install the latest version with the project’s package manager (npm, pnpm, yarn or bun for JavaScript). The versions below are the latest at the time of this docs build. The same list as JSON is https://docs.whisperr.net/sdk-versions.json.

PlatformInstallLatest
Webnpm i @whisperr/web0.4.0
Reactnpm i @whisperr/react @whisperr/web0.4.0
Next.jsnpm i @whisperr/next @whisperr/web0.4.0
Exponpx expo install @whisperr/react-native @react-native-async-storage/async-storage0.7.0
Expo, when the app already has push with expo-notificationsAdd @whisperr/expo to the same command, and expo-constants or expo-application when the app does not have them. See @whisperr/expo.0.2.0
React Nativenpm i @whisperr/react-native @react-native-async-storage/async-storage, then npx pod-install before the next iOS build0.7.0
Flutterflutter pub add whisperr. With push through firebase_messaging, see step 7 first.0.8.0
SwiftSwift package https://github.com/WhisperrAI/whisperr-swift.git, from 0.7.0, product Whisperr on the app target0.7.0
Node.jsnpm i @whisperr/node0.2.0
Pythonpip install whisperr0.2.1
PHP, Laravelcomposer require whisperr/php0.3.0
.NETdotnet add package Whisperr. ASP.NET Core: also dotnet add package Whisperr.AspNetCore1.1.0

The install must only add packages. It must not re-resolve the app’s dependency tree.

  • Use the package manager that owns the lockfile: package-lock.json is npm, pnpm-lock.yaml is pnpm, yarn.lock is Yarn, bun.lock or bun.lockb is Bun. npm i in the table above means pnpm add, yarn add or bun add in those projects. npx expo install picks the right one.
  • Install only packages the app does not have yet. Do not list a package that is already in package.json.
  • Use the install flags the project already uses. When its README, CI or .npmrc uses legacy-peer-deps, pass it too: npx expo install @whisperr/react-native @react-native-async-storage/async-storage -- --legacy-peer-deps (with npm, npm i <packages> --legacy-peer-deps).
  • When the install stops with a peer-dependency error (npm ERESOLVE), read the Could not resolve dependency lines. When they name no @whisperr/ package, the conflict is between packages the app already has. Install the Whisperr packages with --legacy-peer-deps and leave the conflict to the user. When they name a @whisperr/ package, stop and tell the user the versions in the error.
  • Never fix a conflict with --force, npm dedupe, npm audit fix, a plain npm install, or by deleting the lockfile or node_modules.
  • Then read the lockfile diff. New entries for the packages you installed and their dependencies are expected. When a package that was already locked is gone or has another version, restore package.json and the lockfile with git checkout, and install again with the project’s flags. When that happens again, stop and tell the user.

@whisperr/react-native alone sends identify calls and events in Expo. It has no native code, so it needs no config plugin, no prebuild and no new build, and it works in Expo Go.

Install @whisperr/expo only when the app already has push with expo-notifications (step 7). Its config plugin changes native code, so add the plugin to the app config only when the user agrees to a new build.

npx expo install adds the config plugin of a package to plugins by itself when the app config is app.json. With app.config.js or app.config.ts it prints the entry to add instead. Until the user agrees to a new build, remove "@whisperr/expo" when it was added, and do not add it when the CLI asks you to. Keep the rest of the install. Tell the user that push works without the plugin when the app already sets up expo-notifications, and that the plugin needs a new build. expo-constants and expo-application are native modules too: when the app does not have them yet, a development build needs a rebuild before it can load them. Expo Go includes them.

After the install, check the lockfile. The user’s email choice (emailOptedIn in step 5) needs at least:

PackageVersion
@whisperr/web, @whisperr/react, @whisperr/next0.4.0
@whisperr/react-native (React Native and Expo)0.6.0
Flutter whisperr0.7.0
Swift Whisperr0.6.0

Swift in a plain Xcode project: ask the user to add the package in Xcode (File → Add Package Dependencies, the URL above, product Whisperr on the app target). Do not edit project.pbxproj by hand. With Tuist or XcodeGen, add the package to that manifest and generate the project again.

3. Put the key in a public environment variable

Section titled “3. Put the key in a public environment variable”

The publishable key (wpk_…) is made for client code: anyone can read it from the app. Put it in the platform’s public variable. Add the variable name, without the value, to .env.example when the project has one. When the project has no environment variables at all, a constant in the module that initializes Whisperr also works.

PlatformVariableRead it with
Web or React with ViteVITE_WHISPERR_KEYimport.meta.env.VITE_WHISPERR_KEY
React with Create React AppREACT_APP_WHISPERR_KEYprocess.env.REACT_APP_WHISPERR_KEY
Next.jsNEXT_PUBLIC_WHISPERR_KEYprocess.env.NEXT_PUBLIC_WHISPERR_KEY
ExpoEXPO_PUBLIC_WHISPERR_KEYprocess.env.EXPO_PUBLIC_WHISPERR_KEY
React NativeWHISPERR_KEY with the project’s config package, such as react-native-config. Without one, a constant in the init module.Config.WHISPERR_KEY
Flutter--dart-define=WHISPERR_KEY=wpk_…const String.fromEnvironment('WHISPERR_KEY')
SwiftWHISPERR_PUBLISHABLE_KEY in the app’s .xcconfig, mapped to the Info.plist key WhisperrPublishableKey as $(WHISPERR_PUBLISHABLE_KEY)Bundle.main.object(forInfoDictionaryKey: "WhisperrPublishableKey")
  • Next.js builds the value into the bundle. Tell the user to set it in the hosting provider and build again.
  • EAS builds need the variable in the EAS environment too.
  • Flutter: add the define to the run configurations and CI.

The server key (wrk_…) is different. It never goes in client code or in a public variable. A server SDK reads it from WHISPERR_API_KEY in the server’s secret store. Ask the user to set it there. Do not ask them to paste it into the chat.

src/lib/whisperr.ts
import { Whisperr } from "@whisperr/web";
const key = import.meta.env.VITE_WHISPERR_KEY;
// Without a key (a preview or CI build), the SDK stays off.
export const whisperr = Whisperr.init({ apiKey: key ?? "", disabled: !key });

Import this module once from the app entry. Whisperr.init() returns the same client on every call and does nothing during server-side rendering.

Call identify at three moments:

  • right after sign-up succeeds,
  • right after login succeeds,
  • on every app start when a user is already signed in. This also covers users who signed in before the install.

On logout, track user_logged_out, then call reset().

whisperr.identify(user.id, {
traits: { first_name: user.firstName },
email: user.email,
emailOptedIn: user.agreedToEmail, // see the table below; leave it out when the app does not know
});
// Logout:
whisperr.track("user_logged_out");
whisperr.reset();

React and Next.js: the same calls on the client from useWhisperr(), when the auth state resolves.

Rules for every platform:

  • Use the app’s stable user id, never the email address.
  • first_name is the only name trait. Never send a last name, full name, email, phone, address or birth date as a trait or an event property.
  • Send the email only as email. Send no phone number.
  • Leave out a trait or the email when the app does not have it.
  • When the app has its own language setting, call setAppLanguage("<BCP 47 tag>") with the language the user picked. It is not the device language.

emailOptedIn is the user’s own email choice. Never set it to true for a user who did not agree. Then follow the Email permission line:

Email permissionWhat the app sendsWhat Whisperr does
Yesemail. Add emailOptedIn: false when the user declined or turned email off in the app. When the app asks the user, send the answer.Opts the address in, unless emailOptedIn is false.
Per useremail with emailOptedIn: true for a user who agreed in the app (a signup checkbox or a setting), false for a user who declined.Opts the address in only with emailOptedIn: true.
Noemail, with emailOptedIn when the app asks the user.Stores the address as pending. Only the backend can confirm it, with the server key.
Not answered, or no lineDecide from the app’s code: see below.Stores the address as pending until the user answers. Then Whisperr checks the address again under the answer.

No Email permission line, or Not answered: read the app. Find how the app itself emails users today:

  • Email is on by default. The app emails the signup address unless the user opts out, for example with an unsubscribe link or a Settings toggle that starts on. Then the signup gives permission to email. Send email as for Yes: emailOptedIn: false only for a user who turned email off, and no emailOptedIn otherwise. Report email as permitted, with one step for the user: answer Yes to Does your signup get permission to email users?. Until they answer, the addresses wait as pending. When they answer, Whisperr checks them again and opts them in.
  • Email needs the user’s opt-in. The app emails only users who agreed, for example with a checkbox or a Settings toggle that starts off. Send email as for Per user, and tell the user to answer Per user.
  • The app sends no email to users. Send email as for No, and tell the user to answer No. Whisperr then does not email these users unless their backend confirms consent with the server key.

Then:

  • Per user, and the app never asks users about email: every address stays pending. Tell the user. They can add an email choice to the app, answer Yes if every signup agrees to email, or confirm consent from a backend.
  • No, and the repository has a backend: confirm consent there with the server key for users who agreed to email. See Server SDKs.
  • No, and there is no backend: tell the user that Whisperr cannot email these users until they change the answer, or until a backend confirms consent.
  • Not answered, or no line: tell the user which answer fits the app and where to give it: Home → Connect your code or Settings → API keys.

Whisperr also keeps every earlier opt-out, including the unsubscribe link, and it limits what the app key can do with email. For example, the app cannot change a user’s existing address, and one address belongs to one user. See Contact consent.

Before you pick events, map the whole app, unless you already did it in the MCP steps: every screen or route, server handler, background job and webhook. Read the router, the data models and the server handlers. Note the core loop (the action users come back for) and where money changes hands. This map is the event inventory.

Wire every base event that the app really has, without asking. When the app does not have one, skip it and give the reason in your summary.

EventWhen to track it
account_createdOnce per user, right after a new account is created.
The app’s core actionEach time the user does what the app is for.
The core action’s attemptWhen the user starts the core action, if starting and finishing are separate moments. When the app has it.
user_logged_outOn logout, before reset().
account_deletedOnce the account deletion succeeds, from the server when the repository has one. See Account deletion and trials below. When the app has it.
onboarding_started, onboarding_completedWhen setup or onboarding starts and finishes. When the app has it.
goal_set, goal_completedWhen the user sets a goal, and when they reach it. When the app has it.
streak_updated, streak_broken, streak_protection_usedWhen a streak grows, ends, or a freeze or repair saves it. When the app has it.
milestone_reachedWhen the user reaches a milestone the app shows them. When the app has it.
item_savedWhen the user saves or favorites the app’s main object. When the app has it.
reminder_set, reminder_disabledWhen the user turns a reminder on or off. When the app has it.
feature_used, premium_feature_usedWhen the user uses a key feature, or a paid one. When the app has it.
limit_hitWhen the user reaches a free-plan or usage limit. When the app has it.
marketing_consent_changedWhen the user changes their email or notification choice in the app. When the app has it.

Name each event after the app’s own moment when a clearer name fits, for example lesson_started for the attempt or habit_created for a goal. With the MCP server, register each one with the slot of the guide it answers, or slot none when no guide fits.

Never fake an event. Track only what the production code really does, never from dev, test or simulation routes.

A new account. Track account_created only on the code path that runs for a new user: the success of a sign-up form, or a sign-in where the auth provider or the app’s server says that the user is new, for example a new-user flag in the response or the server creating the user record. Sign in with Apple, Google sign-in and magic links usually return the same callback for new and returning users, so never track it on every sign-in. When the app cannot tell a new user from a returning one, do not track account_created from the client. Tell the user, and track it from the backend where the user record is created (Server SDKs), when the repository has one.

The core action. Use the Main action in this app line when the prompt has it. Otherwise find it in the app’s code. Do not guess from the app’s name:

  • Look at the main screen or tab and its primary button: what does it create, log, complete or buy?
  • Follow that action to the write that saves it: the API call, the database insert or the local store. Track at that point.
  • Check what the app limits, counts or rewards: free-plan limits, streaks, progress and the stats it shows the user. These usually count the core action.
  • When the app already tracks events with another tool, reuse the moment of its main event, with a Whisperr code.

Pick one action. Name the event after it, object then verb in the past tense. In your summary, name the event, the file where it fires, and why you picked it, so the user can change it. These are examples from different kinds of apps. Do not copy one: pick the name that fits this app.

Kind of appExample core event
Fitnessworkout_logged
Notesnote_created
Language learninglesson_completed
Shoporder_placed
Gamelevel_completed
Budgetingexpense_added

Events already registered. When the prompt lists them, track those codes exactly as written, at the moment their name describes. Do not rename them.

Money. Track money events only when the app really has the feature:

The app hasEvents
A paywallpaywall_viewed, paywall_dismissed
A screen to manage the subscriptionmanage_subscription_viewed
A free trialtrial_started, trial_converted
A subscriptionsubscription_started, subscription_renewed, subscription_cancelled, subscription_expired
One-off purchases, such as credit packs, tips or coin packsOne event per kind of purchase, for example credit_pack_purchased
  • Track the paywall and subscription screen events from the app. paywall_viewed has a placement property that names where the paywall opened. With the MCP server, register both paywall events with slot none.
  • Trial and subscription events come from the backend or the billing tool, never from client code. trial_started has trial_ends_at. When no billing tool sends it, track subscription_payment_failed only from a real failed-payment or billing-retry webhook, never from a dev or simulation route.
  • Send one-off purchases from the backend that records the purchase. Send them from the app only when it has no backend, and only after the store confirms the purchase.

When RevenueCat, Superwall or Adapty handles purchases, connect that tool in the dashboard. It sends the trial and subscription events, so do not track them yourself. The app still tracks the moments that the tool does not see: paywall_viewed, paywall_dismissed and manage_subscription_viewed.

Other events. Also add every other real moment that helps keep users and fits none of the events above, such as an abandoned flow or the main object archived. Do not ask first. With the MCP server, map each moment to a guide slot when one fits, and register the rest with slot none. Ask the user only about a moment you cannot tell happens in production, or one that would carry sensitive data. Name every event in your summary, with its file and line.

There is no cap and no target. Past about 60 events, check for duplicates, heartbeats and UI noise such as renders, scrolls and taps. That is a sanity check, not a cap.

Properties. Send every property the code really has at that point, each from a real variable: ids, plan, amounts, counts, reasons. Never send a property with a made-up, default or placeholder value. Use these names when the app has the value: lifetime_count (how many times this user has done it), is_first_ever, streak_length and, for a trial, trial_ends_at.

Account deletion and trials. When users can delete their account, track account_deleted once the deletion succeeds, from the server when the repository has one (Server SDKs). Then call reset() in the app and do not identify that user again. Whisperr stops scoring and messaging the user. When the app offers a free trial, track trial_started with trial_ends_at from the backend when the trial starts, unless the app uses RevenueCat, Superwall or Adapty (then connect that tool instead). A trial start is not subscription_started.

whisperr.track("<event_code>", { plain_property: "value" });

Rules for events:

  • Track where the action succeeds: after the save, the upload or the API call returns. Not on a button tap, and never during render or build().
  • No heartbeats, timers or polling events.
  • Codes are lowercase snake_case, start with a letter and have at most 64 characters, for example note_created.
  • Do not send the SDK’s automatic events yourself: app_installed, app_updated, app_opened, app_backgrounded, screen_viewed, page_viewed, push_opened, push_received and push_permission_changed.
  • Properties are plain values. No personal data and no free text that users typed.
  • No subscription, renewal or trial events from client code. They come from the billing tool or the backend (one-off purchases: see Money above). With RevenueCat, call Purchases.logIn(user.id) with the same id as identify, and Purchases.logOut() on logout. With Superwall or Adapty, call that SDK’s identify with the same id, and its reset on logout. Tell the user which billing system the app uses. See RevenueCat, Superwall and Adapty.

Do this only when the app already has push notifications. Do not add push to an app that has none: tell the user that push needs a push provider and, on mobile, a new native build, and let them choose where the permission prompt appears.

Whisperr sends push through the customer’s own provider. The user connects it in the dashboard under Integrations → Push. See Connect a push provider.

Install @whisperr/expo (step 2). After identify, and again right after the app’s own permission request:

import { registerForPushNotifications } from "@whisperr/expo";
await registerForPushNotifications({ requestPermission: false });

It sends the token and the permission to Whisperr, keeps both current, and never shows a prompt of its own. Pass tokenType: "device" when the app sends through its own APNs key or Firebase project. In the root layout, call useWhisperrNotificationResponse() and route to its deepLink. Add "@whisperr/expo" to plugins in the app config only when the user agrees to a new build (eas build): the plugin changes native code. See Expo SDK.

Do not start or use emulators, simulators or physical devices, do not install builds on them, do not run the app’s UI, and do not put a proxy in front of api.whisperr.net, unless the user asks you to. The user runs the app.

  1. Run the project’s own type check or lint. The change must add no compiler, type or analyzer errors. Run a build only when it does not start a simulator, install on a device or start the app.
  2. Tell the user which flows to use so that each event fires once. Start with these: sign up with a new account, do the core action, log out. Then name the flows for the other events you wired. A sign-in with an existing account must not send account_created.
  3. With the MCP server, call get_received_events while they do it. Without it, ask the user to watch See events from your app on the dashboard Home. When nothing arrives, check the key in the project: it is the wpk_… key from the prompt, not empty and not a server key (wrk_…).

What the user sees: on Home → Connect your code, and on the Data page, the events that arrived, within about a minute.

Whisperr registers the event codes that it receives from the app on its own, until an event list is saved for the app. So the user can build the plan from the events the app really sends. Before they build it, they can check the list in the dashboard, pick the core event, and remove events they do not want. See What to track.

End with a short summary:

  • whether you used the Whisperr MCP server, and if not, why,
  • the SDK and version you installed, and the file where it starts,
  • where the key lives, and what the user still sets in hosting, EAS or CI,
  • where each event fires (file and line), the core event you picked and why,
  • each base event and money event you checked and whether the app has it, with a one-line reason from the code when it does not; with the MCP server, the same for every must and should guide,
  • the extra events you added, and any you asked the user about,
  • the file and line of the variable behind each property you send,
  • with the MCP server: the install_coverage from commit_registration (or get_install_coverage), and any declared_not_sent properties,
  • what happens to email under their Email permission answer, or the answer that fits the app when the prompt had none, and what they should do about it,
  • any change the install made to app.json or the lockfile, and why,
  • the billing system the app uses,
  • push: wired, or what it would need,
  • the flows to use to check that events arrive.

A backend uses the server key (wrk_…) and passes the user id on every call. Use the same id as the app’s identify.

server.ts
import { createWhisperr } from "@whisperr/node";
const whisperr = createWhisperr({ apiKey: process.env.WHISPERR_API_KEY! });
// Only after your records show that the user agreed to email:
whisperr.identify(user.id, {
channels: [{ type: "email", address: user.email, optedIn: true }],
});
// A fact only the server knows. The name is an example:
whisperr.track(user.id, "payment_failed", { amount_cents: 4900 });

A server-key identify opts an email address in, whatever the Email permission answer. The server SDKs send opted_in: true when you leave optedIn out, so send an email channel from the server only for a user who agreed. The same calls exist in Python, PHP and .NET. See Confirm consent with identify.

SituationWhat to do
No app keyAsk the user to create one: Home → Connect your code → Copy prompt → Add an app key (owners and admins), or Settings → API keys with the kind App.
The app has no sign-inEvents still go out under an anonymous id. Whisperr never messages a user it knows only by an anonymous id, so tell the user that messages need a signed-in user.
No Whisperr SDK for the platformSend events with the ingestion API, or tell the user.
The app already tracks with another analytics toolAdd Whisperr calls next to the existing ones at the same moments. Do not remove the other tool.