Browser Push Notifications
SendSeven lets you collect browser push subscribers and send them notifications through campaigns and flows. There are two ways to ask visitors for push permission:
| Hosted by SendSeven | On your own domain | |
|---|---|---|
| Installation | Nothing to install | One file + one script tag |
| Permission prompt | Opens in a SendSeven-hosted popup window (your-workspace.push.sendseven.com) | Shown directly on your page |
| Notification sender shown by the browser | Your SendSeven push subdomain | Your own domain |
| Trigger moment | Via SendSeven widgets | Anywhere — automatically, or from your own buttons and code (e.g. after checkout) |
| Works on hosted site builders without file access | Yes | No (you must be able to serve a file from your webroot) |
Both modes can be used side by side. Subscribers are stored in the same lists either way.
The two own-domain files are not only for your own buttons. Once they are in place, the push option in your SendSeven newsletter widget stops opening a popup window and asks the visitor on the page instead. Nothing to switch on — see Widget push button: on-page or popup.
Browser push permission is bound to the exact origin it was granted on. Subscribers collected on your SendSeven push subdomain keep working there, but they do not transfer to your own domain — the own-domain list starts fresh.
Prerequisites
- A SendSeven account with the Browser Push channel (created automatically for every workspace)
- A widget ID — any widget from Widgets in the main menu works; the SDK uses it to load your branding, prompt defaults and default lists (the Code snippets tab below fills it in for you)
- For own-domain mode: the ability to place a file at the root of your website
Own-domain setup
Step 1 — Add the service worker file
Create a file at the root of your website (the file must be served from your own origin, at the top level, so it can control the whole site):
importScripts('https://widget.sendseven.com/service-worker.js');
That single line is the whole file. The actual push-handling logic is loaded from SendSeven's CDN, so you never need to update this file — we ship improvements automatically.
Browsers refuse service worker scripts that are served via a redirect. /sendseven-sw.js must answer with 200 OK directly. A server-side rewrite or proxy rule is fine; a 301/302 is not.
Already have a service worker? If your site is a PWA with an existing service worker, don't add a second file — add the importScripts(...) line at the top of your existing service worker instead, and set data-sw-path (below) to its path.
Step 2 — Add the SDK snippet
Add this before the closing </body> tag on every page where you want push functionality:
<script src="https://widget.sendseven.com/push-sdk.js"
data-widget-id="YOUR_WIDGET_ID" async></script>
Settings → Channels → Browser Push → Code snippets generates this tag for you, with your widget selected and your real list IDs already filled in. See Generating snippets and finding list IDs.
Script tag attributes
| Attribute | Required | Default | Description |
|---|---|---|---|
data-widget-id | Yes | — | Widget ID used for branding, language, and default lists |
data-auto | No | widget setting | "true" shows the branded permission prompt automatically, "false" never does. Omit it and the widget's own Auto-request permission setting decides (off by default) |
data-delay | No | widget setting, else 5 | Seconds to wait before the automatic prompt |
data-ask-interval-days | No | widget setting, else 7 | Days to leave a visitor alone after an automatic prompt they did not accept. 0 asks on every page view (not recommended) |
data-sw-path | No | /sendseven-sw.js | Path to the service worker file on your origin |
data-lists | No | — | Comma-separated list IDs new subscribers are added to |
data-language | No | channel setting | "auto" follows the visitor's browser language, or pass a locale code (ar, de, en, es, fr, hr, it, ja, ko, nl, pl, pt, ru, tr, zh). Omit it to use the language configured on the Browser Push channel |
data-fallback-language | No | en | Locale used when the visitor's language is not supported |
"Widget setting" means the value configured for that widget in the app under Widgets → your widget → Subscriptions. The script tag always wins when the attribute is present, so you can keep one dashboard default and override it on a single page.
The prompt's title, description and buttons are translated automatically; any custom texts you configured on the Browser Push channel always win over the translation.
How the automatic prompt behaves
With the automatic prompt enabled, the SDK shows a branded, non-blocking "soft ask" card first; the browser's native permission dialog only appears after the visitor clicks Allow.
The automatic prompt deliberately stays quiet. It is skipped when:
- the visitor already has an active push subscription for this site,
- the visitor previously blocked notifications in their browser (
Notification.permissionisdenied— asking again would show nothing anyway), or - the visitor is inside the ask cooldown.
The cooldown starts when the card is shown, not only when it is dismissed — a visitor who navigates away without answering is not re-asked on the next page view. Its length is taken from data-ask-interval-days, else the widget's Ask again after (days) setting, else 7 days. Set it to 0 to ask on every visit.
These rules apply to the automatic prompt only. An explicit SendSevenPush.prompt() or SendSevenPush.subscribe() — a button the visitor just clicked — ignores the cooldown and always shows the card. It still cannot do anything when the browser permission is denied; that state can only be cleared by the visitor in their browser settings.
Step 3 — Verify the installation
In the SendSeven app, open Settings → Channels → Browser Push → Install on your website, enter your domain, and click Verify installation. SendSeven checks that:
https://your-domain.com/sendseven-sw.jsis reachable,- it is served without a redirect, and
- it imports the SendSeven service worker.
Generating snippets and finding list IDs
data-lists and the listIds option both take newsletter list IDs. You do not have to hunt for them: open Settings → Channels → Browser Push → Code snippets, pick a widget, tick the lists you want, and the tab writes the finished markup with the real IDs substituted:
| Snippet | What it is for |
|---|---|
| Install snippet | The script tag from Step 2, with data-lists, the automatic-prompt attributes and the language you chose |
| Subscribe button | A ready-to-paste button element calling SendSevenPush.prompt(...) with your lists |
| Pre-load queue | The same button plus the queue shim, for pages where the button can be clicked before the async SDK has loaded |
| Advanced | subscribe() with email/externalId, plus isSubscribed() and unsubscribe() |
The generated snippets never include the service worker file — Step 1 is still required.
Individual list IDs are also visible in the list editor: open Settings → Lists and click a list. The editor is addressable per list (the URL ends in the list ID), and the preview card on the right shows the ID with a copy button.
JavaScript API
Once loaded, the SDK exposes window.SendSevenPush:
SendSevenPush.prompt(options?)
Shows the branded soft-ask card, then (on Allow) the native permission dialog, then subscribes. Returns a Promise.
const result = await SendSevenPush.prompt({
listIds: ['LIST_ID_1', 'LIST_ID_2'], // optional, overrides data-lists
email: '[email protected]', // optional, links to an existing contact
externalId: 'user-4711' // optional, your own user reference
});
// result: { subscribed: boolean, reason?: string }
SendSevenPush.subscribe(options?)
Same options and return value as prompt(), but skips the soft-ask card and goes straight to the native permission dialog. Use this when your own UI already asked the question — for example a "Notify me about my order" button on the order confirmation page:
document.querySelector('#order-updates').addEventListener('click', async () => {
const { subscribed } = await SendSevenPush.subscribe({
listIds: ['ORDER_UPDATES_LIST_ID'],
email: order.customerEmail
});
if (subscribed) showToast('You will be notified about your order!');
});
Passing email stores the address with the subscriber: if no contact with that email exists yet, a contact is created with it (instead of an anonymous push subscriber). If a contact with that email already exists, the subscription is kept separate and the address is stored as a claimed email on the subscription — because the visitor's identity isn't verified, SendSeven never links a browser to an existing contact based on an email alone. You can merge them manually in the app.
SendSevenPush.isSubscribed()
Returns Promise<boolean> — true when permission is granted and an active push subscription exists.
SendSevenPush.unsubscribe()
Removes the browser-side push subscription. Returns Promise<boolean>.
Result reasons
prompt() / subscribe() resolve with { subscribed: false, reason } when subscription did not happen:
| Reason | Meaning |
|---|---|
dismissed | Visitor clicked "Not now" on the soft-ask card (also returned when an automatic prompt was suppressed by the cooldown) |
denied | Browser permission is (or was previously) denied |
already_subscribed | The visitor already has an active subscription. Automatic prompts only — an explicit prompt()/subscribe() re-saves the subscription instead |
unsupported | Browser does not support push notifications |
sw_missing | /sendseven-sw.js was not found on your origin (check the browser console for details) |
sw_error, init_failed, subscribe_failed, save_failed | Technical failures — details in the browser console |
Calling the SDK before it has loaded
The script loads with async, so SendSevenPush may not exist yet when your code runs. Two options:
Ready event:
window.addEventListener('sendseven:push-ready', () => {
// SendSevenPush is fully available here
});
Command queue (safe to call from inline onclick handlers at any time):
<script>
window.SendSevenPush = window.SendSevenPush || { q: [] };
window.SendSevenPush.prompt = window.SendSevenPush.prompt ||
function (o) { window.SendSevenPush.q.push(['prompt', o]); };
window.SendSevenPush.subscribe = window.SendSevenPush.subscribe ||
function (o) { window.SendSevenPush.q.push(['subscribe', o]); };
</script>
<button onclick="SendSevenPush.prompt()">Get updates</button>
Queued calls are executed in order once the SDK loads. Note that queued calls cannot receive the Promise return value — use the ready event if you need the result.
Both Settings → Channels → Browser Push → Code snippets and the Promotion tools tab under Widgets generate copy-paste subscribe-button snippets with your lists pre-selected — including the queue variant above.
Customizing the prompt
The soft-ask card uses your Browser Push channel settings (Settings → Channels → Browser Push): company name, logo, brand color, language, and per-text overrides (title, description, buttonText). The channel language dropdown offers 12 languages; the SDK itself ships 15 locales, so data-language accepts three more (ar, hr, ru).
Widget push button: on-page or popup
When a visitor picks push in a SendSeven newsletter widget, the widget can either hand the request to the on-page SDK or open the hosted popup. This is set per widget under Widgets → your widget → Subscriptions → Subscription flow:
| Setting | Behaviour |
|---|---|
| Automatic (default) | Use the on-page SDK when it is genuinely usable on this site, otherwise open the hosted popup |
| In-page only | Always use the on-page SDK. If it is not installed, push subscription fails — pick this only after the installation verifies |
| Popup window only | Always open the hosted popup, even when the SDK is installed |
On Automatic, the widget probes the page in the background — before the visitor clicks, so the popup fallback never loses its user gesture — and only chooses the on-page path when all of these hold:
- the browser supports service workers and push, on a secure (HTTPS) origin,
- the Push SDK is fully loaded (the pre-load queue shim alone does not count), and
- the service worker stub is really served from your origin: the response must not be HTML, must be non-empty, and must contain
importScripts.
That last check is stricter than "the URL returns 200" on purpose. Single-page-app hosting typically answers any unknown path with 200 OK and index.html, so a plain status check would report a stub that does not exist. If the probe fails, the widget silently uses the popup — which is exactly the legacy behaviour, so nothing breaks; it just means the on-page prompt is not active yet.
If the on-page attempt fails at runtime (missing or broken stub), the widget remembers that for the rest of the browser session and, on Automatic, recovers to the popup. The visitor may have to click once more, because the browser only allows a popup during a fresh click.
Hosted mode
Without any installation, SendSeven widgets offer push subscription through a popup window on your workspace's push subdomain (your-workspace.push.sendseven.com). Visitors grant permission to that subdomain and notifications are delivered from it. This remains the right choice for hosted site builders where you cannot place a file in the webroot.
Welcome message for new subscribers
Each widget can send one push notification immediately after a visitor subscribes — a greeting, a discount code, a link to your latest post. Configure it per widget in the app under Widgets → your widget → Subscriptions; there is no SDK call and no API field for it.
A welcome message has a title, a body, an optional image and an optional click-through URL.
What to expect:
- It fires only on a genuinely new subscription for that browser. A returning visitor who re-subscribes (page reload, service worker update, adding a second list) gets nothing — re-welcoming people is the fastest way to lose push permission.
- Exactly one notification, no matter how many lists the visitor ticked.
- It is a normal push send and is billed like any other push message.
- It works on both paths — the on-page SDK and the hosted popup — as long as the subscription came from a widget that has a welcome message configured.
- Delivery is best-effort: if the welcome message cannot be prepared, the subscription itself still succeeds.
Limitations
- iOS Safari delivers web push only for sites the visitor has added to their home screen (an iOS platform restriction, both modes).
- The service worker file must be at a path that covers the pages where you subscribe visitors — serving it from the webroot is strongly recommended. Deeper scopes require the
Service-Worker-Allowedresponse header. - Subscribers are origin-bound: switching between hosted and own-domain mode does not migrate existing subscribers.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
reason: 'sw_missing' and a console error | The stub file is missing, or your server answers unknown paths with index.html (common on SPAs — add an explicit route for /sendseven-sw.js) |
| Verify fails with "redirect" | Your server redirects the file (e.g. to a canonical host or trailing slash). Serve it with a direct 200 |
| The automatic prompt never appears | In order of likelihood: the automatic prompt is off (neither data-auto="true" nor the widget's Auto-request permission setting), this browser is already subscribed, permission was previously denied, or you are inside the ask cooldown. Your own prompt() button is unaffected by the cooldown |
| It worked once, now the prompt is gone (while testing) | The cooldown is remembered per browser. Test in a fresh private window, or set data-ask-interval-days="0" on a staging page |
| The widget still opens a popup | The on-page probe rejected this page: the SDK script is missing or still loading, the page is not HTTPS, permission is already denied on your origin, or /sendseven-sw.js did not pass the content check (HTML body, empty body, or no importScripts — SPA catch-all routing is the usual cause). Run Verify installation and check the browser console |
| The widget opens a popup even though everything is installed | The widget's Subscription flow is set to Popup window only, or an earlier attempt in this browser session failed and the widget fell back for the rest of the session. Reload the page after fixing the stub |
| Notification permission dialog blocked by the browser | Some browsers require a user gesture — trigger subscribe()/prompt() from a click handler |
| No welcome message arrived | It is per widget, so check that the subscription came from a widget with a welcome message enabled — and note it only fires for a browser that is genuinely new; re-subscribing never re-sends it |
| No image on my notification | Hero images are optional in the Web Push standard. Chromium-based browsers on Windows, Linux and Android render them; Safari and Firefox show title and body only. Never put essential information in the image |