Identify users
Last updated
Use POST /v1/identify to tell Whisperr who a user is: traits that give
messages context, and the contact channels Whisperr may use to reach them.
It also links events that the user sent before login to the user.
POST /v1/identifyIdentify is idempotent. Call it after login, and on every app start or session restore while a user is signed in. This also covers users who signed in before you added Whisperr. Traits are merged into the stored ones.
Send an identify request
Section titled “Send an identify request”This request comes from your backend with the server key, after the user agreed to email:
curl https://api.whisperr.net/v1/identify \ -H "X-API-Key: $WHISPERR_SERVER_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_user_id": "user_8842", "traits": { "first_name": "Ada", "plan": "pro", "timezone": "Europe/Berlin" }, "channels": [ { "channel": "email", "address": "ada@example.com", "opted_in": true, "verified": true } ] }'A valid request returns 202 Accepted:
{ "processing": "queued" }Whisperr applies the traits and channels right after, normally within seconds. Identify and events are applied in the order Whisperr accepted them, so an event sent after an identify sees its result.
The identify object
Section titled “The identify object”external_user_id string
Section titled “external_user_id string”Your own stable user id: the same one you send on events. Required, except in an anonymous identify.
anonymous_id string
Section titled “anonymous_id string”The handle this device sent events under before login, 1 to 128 printable
characters. With external_user_id, Whisperr moves the handle’s events,
activity and push tokens to the identified user. An unknown handle is not an
error. The browser SDK sends it for you.
traits object
Section titled “traits object”Free-form user attributes, up to 50 KB as JSON. Omit the field when empty.
Send only what messages need, such as first_name for a greeting. Do not send
a full name, last name, email address or phone number as a trait. Send
contact addresses as channels.
Three trait keys are reserved. Whisperr reads them to decide when to send and in which language:
| Key | Format | Used for |
|---|---|---|
timezone | IANA name, such as Europe/Berlin | Quiet hours and send time. Without it, Whisperr uses UTC. |
locale | BCP 47 tag, such as de-DE | The device language. A fallback for the message language. |
app_language | BCP 47 tag, such as de | The language the user picked in your product. Whisperr writes messages in it first. |
The client SDKs fill in timezone and locale from the device. Server SDKs
never guess them. Put these keys inside traits: a top-level timezone is an
unknown field and fails the request.
Which traits does Whisperr drop?
Section titled “Which traits does Whisperr drop?”Whisperr does not store the traits below. The request still succeeds.
| Trait | Example | What Whisperr does |
|---|---|---|
| Value is a secret | A card number, an IBAN, a JWT, or an API key in a known format (sk_live_…, ghp_…) | Never stores it. |
| Key is a secret key and the value is a string or number | password, card_number, iban, ssn | Never stores it. |
| Value holds an email address or is a phone number | "ada@example.com", "+44 7700 900123" | Does not store it as a trait. Send an email address in channels. |
| Key is a special-category key | religion, mental_health, medical_history, biometric_data | Does not store it, unless a workspace owner or admin allows that key. |
Whisperr checks each nested field and array item too. It removes only the field that matches.
A key name alone does not make a trait a contact address:
"marketing_email": true and "mail": "weekly" are stored.
Exact key lists
Section titled “Exact key lists”Whisperr compares keys after it changes them to lower case and changes
camelCase, -, . and spaces to _. So mentalHealth and
mental-health both match mental_health.
- Secret keys:
password,passwd,passcode,pin_code,card_number,cardnumber,credit_card,creditcard,cc_number,cvv,cvc,card_cvc,iban,account_number,bank_account,bank_account_number,routing_number,sort_code,ssn,social_security_number,national_id,national_insurance_number,passport,passport_number,tax_id,driver_license,drivers_license,driving_licence,personal_id_number, and any key that ends in_password. - Special-category keys:
health_condition,health_conditions,mental_health,diagnosis,diagnoses,medication,medications,disability,pregnancy,religion,sexuality,sexual_orientation,sex_life,gender_identity,ethnicity,racial_origin,trade_union,biometric_data, and any key that starts withmedical_,religious_,ethnic_,political_,trade_union_orgenetic_.
Keys that are not on these lists are stored. This includes health,
politics, race, biometric and genetic: many products use these words
for other data, such as hit points or a news topic. Do not send
special-category data under a key that is not on the list.
Whisperr records each dropped key, the reason and a count. It never records
the value. A workspace owner or admin can read this list with the Whisperr
MCP tool list_traits (see Use with your AI agent).
preferred_channel string
Section titled “preferred_channel string”email or push. Optional. When it is set and the channel can reach the
user, Whisperr uses it for that user. Without it, Whisperr picks the channel.
Set it only to record a choice the user made.
channels array
Section titled “channels array”Up to 10 contact channels. Each item:
| Field | Type | Notes |
|---|---|---|
channel | string, required | email or push. The field is named channel, not type. The API rejects unknown fields, so type fails the whole request with a 400. |
address | string, required | An email address, or a push token. |
opted_in | bool, optional | false records an opt-out with any key: Whisperr will not use that address. With the server key, an omitted opted_in opts in a new email address and confirms a pending one. An address that already holds a decision keeps it. With the publishable key, a new email address is opted in only under your consent answer: Yes opts it in unless opted_in is false, Per user only when it is true and consent_source is "user", and No stores it as pending. Require a server key makes every one pending. An earlier opt-out, including the unsubscribe link and bounce or complaint suppression, is kept. A later identify from the app never opts the address back in. The limits in Contact consent apply. |
consent_source | string, optional, email only | "user" when the user agreed to email in your app. Send it with opted_in: true. Per user needs it. The client SDKs send it for emailOptedIn: true. Whisperr ignores any other value. |
verified | bool, optional | Only the server key can verify an address. Whisperr ignores verified from the publishable key. With the server key, an omitted verified counts as true, unless the address was last declared false. Whisperr does not email an address that your server declared unverified. |
kind | string, optional, push only | fcm, apns, expo or onesignal_sub: the token type. It tells Whisperr which provider can send to the token. Without it, Whisperr infers it from the token: ExponentPushToken[…] or ExpoPushToken[…] is expo, 64 hex characters is apns, anything else is fcm. Send a OneSignal subscription id with kind: "onesignal_sub". |
platform | string, optional, push only | ios, android, web, macos, windows or linux. |
push_env | string, optional, push only | production or sandbox: the APNs environment of the token. Send it with kind: "apns". |
client_app_id | string, optional, push only | The app that owns the token: iOS bundle id, Android package name or web origin. |
provider_project | string, optional, push only | The provider project of the token: Firebase project id (fcm), EAS project id (expo) or OneSignal app id (onesignal_sub). Never for apns. |
Push tokens follow the device permission instead. The mobile SDKs send a push token opted in only while the OS reports notification permission. See Contact consent.
The five push fields never fail a request. Whisperr drops a value it does not
know, and drops the fields on an email channel. See
Connect a push provider for how Whisperr
routes a token by its kind.
With the publishable key, an identify that gives a user a different email address than the one stored is rejected after acceptance. Change an existing address from your backend with the server key. See Rejections after acceptance.
Anonymous identify
Section titled “Anonymous identify”An identify with anonymous_id and no external_user_id stores data for a
visitor you do not know yet. The SDKs use it to register a push token or an
app language before login.
{ "anonymous_id": "3f2c9a1e-5b7d-4e8f-a0c1-2d3e4f5a6b7c", "traits": { "app_language": "de" }, "channels": [ { "channel": "push", "address": "<device token>", "opted_in": true, "kind": "fcm", "platform": "android" } ]}- It accepts only
traitsandpushchannels. Anemailchannel or apreferred_channelin an anonymous identify is a400. - A later identify with the same
anonymous_idand anexternal_user_idmoves the push tokens to the identified user. Traits from an anonymous identify do not move, so send them again on the identified call. The SDKs do this forapp_language. - When the handle already belongs to an identified user, Whisperr ignores the anonymous identify.
SDK shortcuts
Section titled “SDK shortcuts”The client SDKs take email and emailOptedIn as a shortcut for the email
channel. In an app, they run with the publishable key, so your answer to
Does your signup get permission to email users? decides what happens:
// In the app (publishable key). With "Yes", the address is opted in.// With "Per user", only when emailOptedIn is true. With "No", it is pending.whisperr.identify("user_8842", { traits: { first_name: "Ada", plan: "pro" }, email: "ada@example.com", emailOptedIn: true,});An earlier opt-out, including the unsubscribe link and bounce or complaint suppression, is kept. A later identify from the app never opts the address back in. The shortcut needs a client SDK version with app-side consent. See Contact consent.
The server SDKs (Node, Python, PHP, .NET) behave differently: their email
shortcut sends opted_in: true, so it opts the address in. Use it from your
backend only when the user agreed to email:
// On your backend (server key), after the user agreed to email.whisperr.identify("user_8842", { channels: [{ type: "email", address: "ada@example.com", optedIn: true, verified: true }],});The SDKs take type and send it as the wire field channel. The backend call
is the same request as the curl example above, without traits.
Common questions
When should I call identify?
After login, and on every app start or session restore while a user is signed in. Identify is idempotent, and a call that repeats what Whisperr already stores changes nothing.
Can the publishable key opt a user in to email?
Only when you told Whisperr that your signup gets permission to email users. With Yes, a new address from the app is opted in unless opted_in is false. With Per user, only when opted_in is true and consent_source is "user". With No, or with Require a server key on, it is stored as pending until your backend confirms it with the server key.
Why does identify fail with 400 when I send type in a channel?
The wire field is channel, not type. The Whisperr API rejects unknown fields, so type fails the whole request. The SDKs take type in their own API and send channel.
Why was my trait not stored?
Whisperr drops traits that hold secrets, contact addresses or special-category data. The request still succeeds. The Whisperr MCP tool list_traits shows each dropped key and the reason.