Skip to content

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/identify

Identify 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.

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.

Your own stable user id: the same one you send on events. Required, except in an anonymous identify.

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.

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:

KeyFormatUsed for
timezoneIANA name, such as Europe/BerlinQuiet hours and send time. Without it, Whisperr uses UTC.
localeBCP 47 tag, such as de-DEThe device language. A fallback for the message language.
app_languageBCP 47 tag, such as deThe 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.

Whisperr does not store the traits below. The request still succeeds.

TraitExampleWhat Whisperr does
Value is a secretA 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 numberpassword, card_number, iban, ssnNever 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 keyreligion, mental_health, medical_history, biometric_dataDoes 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.

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 with medical_, religious_, ethnic_, political_, trade_union_ or genetic_.

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).

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.

Up to 10 contact channels. Each item:

FieldTypeNotes
channelstring, requiredemail or push. The field is named channel, not type. The API rejects unknown fields, so type fails the whole request with a 400.
addressstring, requiredAn email address, or a push token.
opted_inbool, optionalfalse 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_sourcestring, 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.
verifiedbool, optionalOnly 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.
kindstring, optional, push onlyfcm, 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".
platformstring, optional, push onlyios, android, web, macos, windows or linux.
push_envstring, optional, push onlyproduction or sandbox: the APNs environment of the token. Send it with kind: "apns".
client_app_idstring, optional, push onlyThe app that owns the token: iOS bundle id, Android package name or web origin.
provider_projectstring, optional, push onlyThe 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.

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 traits and push channels. An email channel or a preferred_channel in an anonymous identify is a 400.
  • A later identify with the same anonymous_id and an external_user_id moves 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 for app_language.
  • When the handle already belongs to an identified user, Whisperr ignores the anonymous identify.

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:

app.ts
// 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:

server.ts
// 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.