1. Guides
  2. Embedded recipient settings

​
Embedded recipient settings

Your users have to connect a messenger somewhere. This is the piece that lets that somewhere be your own notification-settings page: one call from your backend, one custom element in your markup, and the recipient never leaves your brand except for the provider’s own consent screen.

Three things make it safe to put in a browser:

  • your project credential never leaves your backend — the browser gets a short-lived capability that can do nothing but manage one subscriber’s own channels;
  • origins and return destinations are configured by you, in advance, so nothing a page sends can widen where WhooshBang will talk or where it will send somebody afterwards; and
  • the capability is not accepted from a URL, so it cannot survive in a browser history, a referrer header, or a shared link.

​
1. Configure the experience once

In the dashboard, open your project’s Settings → Integration & embedding. The dashboard sets the live environment; set test through the API, below.

Project-wide, you set what a recipient sees: the display name, an accent colour, light/dark/system appearance, and the fallback language for anyone whose own language you have not told us.

Per environment — test and live independently — you set:

SettingWhat it means
Enabled channelsWhich channels this environment offers a recipient. A channel can only be enabled while the environment has a healthy connection that can actually complete an authorization.
Allowed originsThe exact scheme://host:port values your settings page is served from. Paste them as your browser gives them: a trailing slash, an uppercase host and an explicit :443 all mean the same origin and are stored in canonical form. Wildcards, paths and parent domains are refused. A page on any other origin is answered with recipient_origin_forbidden and no CORS grant, so the browser will not let that page read the response at all.
Return destinationsNamed keys that resolve to exact URLs. Your backend names a key; WhooshBang resolves it. A caller cannot supply a URL, so this cannot become an open redirect.

The page offers only channels that have a working connection, and shows a live preview of the widget as you change the brand and channels.

​
Or configure it from your own backend

The dashboard is one surface over these settings, not the only one. A project credential carrying projects:read and projects:write reads and writes the same configuration, so an application — or an agent — can set itself up with no browser in the loop:

await whooshBang.updateEnvironmentRecipientExperience(projectId, environmentId, {
  allowed_origins: ["https://app.example.com"],
  enabled_channels: ["telegram"],
  return_destinations: [
    { key: "notifications", url: "https://app.example.com/settings/notifications" },
  ],
  revision: current.revision,
});

Those two scopes are not selected by default when you create a credential, and they should stay off the one your servers use to send. A credential that can only send cannot change where your recipient settings panel may be mounted; adding configuration rights to it would make a leaked sending key a way to move that panel onto somebody else’s page. Create a separate credential for setup if you want both.

revision is the value you last read. A write presenting an older one is refused with 409 rather than overwriting a change somebody else made.

The project-wide settings — display name, accent colour, logo and language — have one value per project, and it is the one live recipients see. Writing them takes a live credential; a test credential reads them, and writes its own environment’s channels, origins and destinations. A test credential attempting the project-wide write is refused with 403 environment_mismatch.

​
2. Create a session from your backend

Your backend authenticates its own user however it already does, then asks WhooshBang for a session for that user:

curl --request POST https://api.whooshbang.com/v1/recipient-sessions \
  -H "authorization: Bearer $WHOOSHBANG_CREDENTIAL" \
  -H "content-type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{"subscriber_id":"user_123","locale":"es","return_to":"notifications"}'

The request is deliberately small. Channels, brand, origins, and return URLs are configuration, not per-request input, so a bug in your session endpoint cannot change any of them.

whooshbang:verify=fixture:recipient-experience-v1/requests/create-recipient-session.json
{
  "subscriber_id": "user_123",
  "locale": "es",
  "return_to": "notifications",
  "expires_in_seconds": 900
}

Creating a session also creates the subscriber if it is new, and records the locale you supplied. It changes nothing else — no groups, no channels, no consent.

Retrying with the same idempotency-key is safe and returns the same session. The browser_capability in that reply is a fresh one, because WhooshBang keeps only a fingerprint of the original and cannot hand it back — so the earlier one stops working. Use whichever reply you actually received. Reusing a key with a different body is a conflict, as everywhere else.

The response is the only time the browser capability is ever returned. Hand the whole response to your frontend and store nothing:

whooshbang:verify=fixture:recipient-experience-v1/success/recipient-session-created.json
{
  "session": {
    "id": "recipient_session_user_123",
    "capability_prefix": "wb_rs1.rsc_AAAAAAAAAAAAAAAAAAAAAA",
    "state": "active",
    "configuration_revision": 2,
    "locale": "es",
    "brand": {
      "display_name": "Acme Alerts",
      "logo_asset_id": "asset_acme_recipient_logo",
      "accent_color": "#4F46E5",
      "color_scheme": "system"
    },
    "channels": [
      {
        "channel": "telegram",
        "state": "available",
        "preferred": false
      }
    ],
    "operations": [
      "session:read",
      "handoff:create",
      "binding:pause",
      "binding:resume",
      "binding:revoke",
      "preference:write"
    ],
    "expires_at": "2026-08-22T18:15:00Z",
    "diagnostic_id": "diag_n17_recipient_session_created"
  },
  "browser_capability": "wb_rs1.rsc_AAAAAAAAAAAAAAAAAAAAAA.BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
  "hosted_url": "https://connect.whooshbang.com/recipient-settings"
}

If that capability may have leaked, end the session from the same backend that created it. The credential needs subscriptions:write; the browser capability is not sent back to revoke itself:

await whooshBang.revokeRecipientSession(session.session.id);

The SDK method is revokeRecipientSession(sessionId), which sends DELETE /v1/recipient-sessions/{recipient_session_id}. Repeating the call is safe. Revocation also closes any provider handoff the session created but has not yet exchanged, so ending the browser capability does not leave a second authorization door open until its original expiry.

​
3. Mount recipient settings

The component is a standards-based custom element. React, Vue, Svelte, a server-rendered template and a plain HTML file all use the same tag and the same property.

The browser entry points are @whooshbang/sdk/recipient and @whooshbang/sdk/recipient/element. The organization contract batch uses @whooshbang/sdk@1.0.0-rc.32. After publication, install that SDK version for both server calls and the browser surface, and pin it exactly in your deployment lockfile. Do not use next as the release-evidence pin.

The recipient surface is a browser-only SDK subpath that you serve from your own origin — build it into your bundle, or copy its compiled modules next to your other assets. The SDK root remains safe for server and Worker imports and does not register a custom element. A complete, runnable version of both integrations below lives in apps/recipient-example in the WhooshBang repository.

<script type="module" src="/assets/whooshbang-recipient.js"></script>
<whooshbang-recipient-settings id="notifications"></whooshbang-recipient-settings>
<script type="module">
  const response = await fetch("/api/whooshbang/recipient-session", {
    method: "POST",
  });
  const element = document.querySelector("#notifications");
  element.configuration = { baseUrl: "https://api.whooshbang.com" };
  element.session = await response.json();
</script>

configuration.baseUrl is where your browser talks to WhooshBang. Point it at a local API while you are developing; production is the value above.

The session is a JavaScript property, not an attribute. That is deliberate: an attribute is something a server-rendered page prints into HTML, and the capability must never appear in markup.

In a framework, it is the same element and the same property:

import { useEffect, useRef } from "react";
import "@whooshbang/sdk/recipient/element";

export function NotificationSettings({ session }) {
  const host = useRef(null);
  useEffect(() => {
    const element = host.current;
    if (!element || !session) {
      return undefined;
    }
    element.configuration = { baseUrl: "https://api.whooshbang.com" };
    element.session = session;
    // Releases the session and closes any provider window it opened.
    return () => {
      element.session = null;
    };
  }, [session]);
  return <whooshbang-recipient-settings ref={host} />;
}

Importing the package registers the element; there is nothing else to call.

If your host cannot carry a custom-element tag at all, mountRecipientSettings does the same thing imperatively:

import { mountRecipientSettings } from "@whooshbang/sdk/recipient";

const mounted = mountRecipientSettings({
  session,
  target: document.querySelector("#notifications"),
});

​
Language

The panel speaks the recipient’s own language. Every word it renders — the heading, the controls, the two-step disconnect warning, the sentence explaining why a paused channel is not receiving anything — ships in Arabic, Dutch, English, French, German, Hindi, Indonesian, Italian, Persian, Polish, Portuguese (Brazil), Russian, Simplified Chinese, Spanish, Turkish, and Ukrainian. Arabic and Persian render right to left.

Which one a recipient gets is settled before the session reaches the browser: the locale you sent, else the one WhooshBang already recorded for that subscriber, else the project’s default_locale. You pass a language tag and nothing else; there is no catalogue to load, no bundle to pick, and no configuration in the mount call.

An Arabic-speaking recipient of an English-language application therefore reads their own settings in Arabic, the right way round, inside your page.

​
Events

The element dispatches bounded events for your own analytics and navigation: ready, handoff-started, binding-changed, completed, and error. Their details carry only safe identifiers and closed categories — never the capability, the provider identity, the resolved return URL, or the subscriber ID.

​
4. What the recipient does

They press Connect Telegram. WhooshBang opens its own hosted page in a popup, which hands them the exact bot for this environment; they press Allow in the chat, and the panel updates itself.

Everything that goes wrong here is a state the panel shows rather than an error it swallows:

  • the browser blocks the popup — the panel offers a link they can open themselves, and keeps watching for the result either way;
  • they close the popup — the panel notices, says so, and offers to connect again;
  • they reload your page — your backend mints a fresh session, the panel shows whatever they had already connected, and no second binding is created;
  • the session expires — the panel says so, and your page recovers by assigning a fresh session from your backend.

Completion is reported to your page through an exact-origin postMessage that also has to echo a nonce the component generated. A page cannot forge it, and WhooshBang never posts to *.

​
5. Preferences, pausing, and disconnecting

The first channel a recipient connects becomes the one their notifications go to. If they later connect a second, each connected channel offers Use for notifications, and nothing moves until they press it.

An ordinary send resolves that exact choice:

curl --request POST https://api.whooshbang.com/v1/messages \
  -H "authorization: Bearer $WHOOSHBANG_CREDENTIAL" \
  -H "content-type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{"to":{"subscriber_id":"user_123"},"content":{"type":"text","text":"Your trial ends tomorrow."}}'

A paused, disconnected, or unavailable preferred channel does not fall back. The send is refused with preferred_binding_unavailable, no provider is called, and nothing is delivered anywhere the recipient did not choose. That is the point: a person who paused Telegram has not consented to being messaged somewhere else instead. Their next explicit choice in the panel is what restores delivery.

A subscriber who has never used the panel keeps the behaviour they had before this existed: the environment’s default connection.

​
6. Redirect instead of a popup

If you would rather not use a popup at all, create the session with a return_to key and set completionMode:

element.configuration = {
  baseUrl: "https://api.whooshbang.com",
  completionMode: "redirect",
};

The whole tab goes to WhooshBang and comes back to the exact URL that key resolves to, and to nothing else. A session created without a return_to key refuses to start a redirect handoff, because there would be nowhere to return the recipient to.

​
What this surface will not do

  • It accepts no arbitrary CSS, HTML, script, remote font, or tracking asset. Brand is a display name, an accent colour, an appearance, and an asset WhooshBang hosts.
  • It never relabels a provider’s own consent screen. The shared WhooshBang bot identifies WhooshBang.
  • It is not a preference centre. Your business notification categories are yours; this manages the channel a person receives on.