Skip to content

Contact consent

Last updated

Whisperr sends a message only to a contact with a recorded opt-in. This page explains who can record that opt-in for email: your backend with the server key, and your app with the publishable key when you allow it for the app. It also covers the limits on addresses from the app, and how push consent follows the device permission.

Does your signup get permission to email users?

Section titled “Does your signup get permission to email users?”

Whisperr asks this during setup. Your answer decides what happens to an email address that your app or website sends with the publishable key:

AnswerSetting valueAn email address from your app
YesassumedIs opted in, unless the app sends optedIn: false. Use it when every user who signs up agrees to get email.
Per userper_userIs opted in only when the app sends optedIn: true (on the API: opted_in: true with consent_source: "user"). Use it when each user makes their own choice in your app.
NooffIs stored as pending: not opted in. Your backend confirms it with the server key.
Not answerednoneThe same as No.

With every answer, 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 setting is per app. The API name is client_email_consent. You can answer it or change it in Settings → API keys.

When you change your answer, or turn off Require a server key, Whisperr checks the pending addresses from your app again under the new answer. These re-checks do not count toward the hourly limit below. The other limits still apply: one user per address, and earlier opt-outs and suppression are kept. It uses the choice that the app sent with each address. An address stored before Whisperr kept that choice is not checked again: the next identify of that user from the app applies the new answer.

In the app, send the user’s choice on the email channel:

app.ts
// In the app (publishable key).
whisperr.identify(user.id, {
channels: [{ type: "email", address: user.email, optedIn: user.agreedToEmail }],
});

The client SDKs also take the shortcut email with emailOptedIn, for example identify(user.id, { email: user.email, emailOptedIn: true }). It sends the same channel. Leave emailOptedIn out when the app does not know the user’s choice.

SDKVersion with app-side consent
@whisperr/react-native (React Native and Expo)0.6.0 or later
@whisperr/web (also @whisperr/react and @whisperr/next)0.4.0 or later
Flutter whisperr0.7.0 or later
Swift Whisperr0.6.0 or later

Earlier versions cannot set app-side consent. Whisperr stores an email address they send as pending, whatever your answer. Older Web, Flutter and Swift versions send opted_in: true on the email shortcut by default, so Whisperr does not treat it as the user’s choice.

optedIn: false records an opt-out with any key and any answer.

KeyWhere it runsEmail contacts
Publishable key (wpk_…)Apps and browsersCan send an address. Whether it is opted in depends on your answer above. Can record an opt-out (opted_in: false). Cannot change a user’s existing address.
Server key (wrk_…)Your backend onlyCan mark an address verified and opted in, whatever your answer. Can record an opt-out. Can change a user’s existing address.
  • Whisperr ignores verified: true from the publishable key.
  • Push tokens are different. See Push notifications.
  • Whisperr sends push and email only. SMS is not available.

Anyone can read a publishable key from your app. So Whisperr limits what the publishable key can do with email, whatever your answer:

  • One address per user. The app cannot change a user’s existing email address. Whisperr answers 403 (contact_change_requires_server_key). Change it from your backend with the server key.
  • One user per address. When another user in your app already has the address opted in, the app cannot opt it in for a second user. Whisperr stores it as pending.
  • Rate limit. By default, the app can opt in up to 200 new email addresses per app in any rolling hour. Whisperr stores addresses over the limit as pending. The identify call still succeeds, and a later identify of the same address tries the opt-in again.
  • Earlier opt-outs stay. 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.
  • Bounces and complaints. A hard bounce opts the address out. A spam complaint opts the user out of email. Whisperr gets both from your email provider’s webhook. See Connect an email provider.
  • Unsubscribe. Every email has an unsubscribe link. A user who unsubscribes is opted out of email. See How does unsubscribe work?

An address stored as pending gets no email until your backend confirms it with the server key.

Require a server key, in Settings → API keys, is a stricter mode. When it is on, only your server can opt an email address in. Addresses your app sets get no messages until your server confirms them, whatever your answer above. When it is off, your answer above decides.

A common setup with a backend: the app sends the address with the publishable key, and your backend confirms consent for the same user id with the server key.

A server-key identify opts an email contact in, whatever your answer to the consent question. Call it from a server SDK (Node, Python, PHP, .NET) for the same user id and address. Send optedIn: true only when the user agreed to email. Send verified: true only when your app confirmed the address, for example with a confirmation link or a one-time code. A server-key event with user.channels works the same way.

server.ts
import { createWhisperr } from "@whisperr/node";
// Runs on your backend. WHISPERR_SERVER_KEY is the server key (wrk_…).
const whisperr = createWhisperr({ apiKey: process.env.WHISPERR_SERVER_KEY! });
whisperr.identify(user.id, {
channels: [
{ type: "email", address: user.email, optedIn: user.emailConsent, verified: user.emailVerified },
],
});

The same request with curl:

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",
"channels": [
{ "channel": "email", "address": "ada@example.com", "opted_in": true, "verified": true }
]
}'

What the server key does when you leave a field out:

  • opted_in omitted: opts in a new address, and confirms a pending address that the app sent first. The order of the two calls does not matter. An address that already holds a decision keeps it.
  • verified omitted: counts as true, unless the address was last declared false.

See Identify users for the full request.

Section titled “Record consent proof with a consent assertion”

Use POST /v1/consent/assertions to record the proof of a consent choice, or an opt-out, for one channel of a user. For example, record which form or settings toggle the user used. It accepts only the server key.

An assertion does not make a pending contact sendable by itself. Whisperr also needs the address opted in, by a server-key identify or by your app under your consent answer. To confirm consent from your backend, send the identify above. Add an assertion when you want to store the proof.

curl https://api.whisperr.net/v1/consent/assertions \
-H "Authorization: Bearer $WHISPERR_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_user_id": "user_8842",
"channel": "email",
"status": "opted_in",
"basis": "explicit_user_action",
"asserted_at": "2026-10-12T09:30:00Z",
"evidence_ref": "settings_toggle:evt_7f3a"
}'
FieldTypeNotes
customer_user_idstring, requiredYour user id, the same external_user_id you send on identify. The user must already exist in Whisperr.
channelstring, requiredemail or push.
statusstring, requiredopted_in, opted_out or unknown.
basisstring, requiredexplicit_user_action: the user did something, such as tick a box. customer_declared: your records say so, with no single user action.
asserted_atstring, requiredWhen the user made the choice, RFC 3339. Not more than 5 minutes in the future.
evidence_refstring, requiredYour reference to the proof, such as a form submission id. 1 to 512 characters. Do not put personal data in it.

Responses from POST /v1/consent/assertions:

StatusBodyMeaning
200{ "ok": true }Recorded.
400{ "error": "…" }A field is missing, unknown or not valid.
401{ "error": "…" }The key is missing, revoked, or a publishable key.
404{ "error": "…" }Whisperr does not know the user yet. Send identify first, then retry after a few seconds.

The choice with the latest asserted_at wins. An exact tie counts as opted_out. After a user unsubscribes from a Whisperr message, only a newer explicit_user_action opt-in from your backend with the server key, or the user’s own choice through Whisperr, lets Whisperr message them again. Nothing your app sends with the publishable key lifts an unsubscribe.

Push consent comes from the device. The mobile SDKs send a push token opted in only while the OS reports notification permission. When the user turns notifications off in the OS, the SDK opts the token out.

Apple’s App Review guideline 4.5.4 asks for more when pushes can carry promotions. This is product guidance, not legal advice:

  • Show a consent line at the push permission prompt. Say that notifications may include offers and reminders.
  • Give users an in-app toggle to turn these notifications off.
  • When the user turns the toggle off, report the permission as denied to the SDK. The SDK opts the push token out and holds it. When the user turns the toggle on again, report the real OS status.
SDKToggle offToggle on
SwiftpushPermissionChanged(.denied)refreshPushPermission()
React NativesetPushPermission("denied")setPushPermission(<OS status>)
FluttersetPushPermission(WhisperrPushPermission.denied)setPushPermission(<OS status>)

Common questions

Why does Whisperr not email a user my app identified?

An email address sent with the publishable key is opted in only when you told Whisperr that your signup gets permission to email users (Yes, or Per user with optedIn: true), Require a server key is off, and your SDK version supports app-side consent. Even then Whisperr keeps it pending when another user in the app already has the address, when the app hit the hourly limit on new addresses, or when the user opted out earlier. Your backend can confirm it with a server-key identify.

Can an app without a backend email its users?

Yes. Answer Yes or Per user to Does your signup get permission to email users? Then an address that the app sends with identify can be opted in, within the limits on this page.

Do the server SDKs opt users in by default?

Yes. The Node, Python, PHP and .NET SDKs send opted_in: true for a channel when you leave optedIn out. Send a channel from them only when you hold consent for it, or pass optedIn: false.

Does a consent assertion make an address sendable?

No. POST /v1/consent/assertions stores proof of a choice. Whisperr also needs the address opted in, by a server-key identify or by your app under your consent answer.

Can a user who unsubscribed be opted in again?

Only by your backend with the server key, with a newer opt-in with basis explicit_user_action, or by the user through Whisperr. A customer_declared opt-in cannot reverse an unsubscribe from a Whisperr message, and nothing your app sends with the publishable key can.