Install Pusharoo on any website
Two files, one line of HTML. Works on any site where you can put a file at the root of your domain and add a script tag — plain HTML, PHP, WordPress, Laravel, Next.js, you name it.
https://yoursite.com/sw.js, and a site key from the Pusharoo dashboard — create a free account to get one (it takes a minute).1. Add your site and get a site key
In the dashboard go to Websites → Add website. Enter the exact address visitors use, e.g. https://yourstore.com (the www. version is accepted automatically). Add your logo URL so your prompt and notifications carry your brand — Pusharoo never shows its own name to your visitors.
You’ll get a public site key like pk_3f9a…. It’s safe to put in your HTML: it only works from the website address you registered (requests from any other origin are refused).
2. Upload sw.js to your site root
Browsers only deliver notifications to a service worker that lives on your domain. Download ours and upload it so it opens at https://yoursite.com/sw.js:
Or from a terminal on your server:
curl -o /path/to/your/webroot/sw.js https://pusharoo.com/sw.jsServe it as JavaScript and don’t let it be cached for long. On Apache, add this to your site’s .htaccess:
<Files "sw.js">
Header set Cache-Control "no-store, max-age=0"
Header set Service-Worker-Allowed "/"
</Files>3. Add the snippet
Paste this before </head> on every page (or in your site’s global header/footer):
<script src="https://pusharoo.com/sdk/pusharoo.js" data-site="pk_your_site_key" async></script>That’s it. After a few seconds a polite, branded prompt appears. The browser’s own permission dialog is only shown after the visitor taps Allow on yours — never on page load. A visitor who taps “Not now” isn’t asked again for a few days, and a browser that says “Block” is never asked again.
A fuller example with timing options:
<script src="https://pusharoo.com/sdk/pusharoo.js"
data-site="pk_your_site_key"
data-delay="8000" data-scroll="30"
data-title="Want first dibs on deals?"
data-text="We send 1–2 alerts a week. Unsubscribe anytime."
data-ios-hint="true" async></script>4. iPhone & iPad (iOS / iPadOS 16.4+)
Apple only allows web push from sites that have been added to the Home Screen. A regular Safari or Chrome tab on iPhone can’t subscribe. The visitor’s steps are:
- Tap Share → Add to Home Screen.
- Open your site from the new Home Screen icon.
- Tap Allow on your prompt, then on the iOS dialog.
For this to work your site needs a small web app manifest. Save this as /manifest.webmanifest (or /manifest.json) at your site root, with your own name and icons:
{
"name": "Your Store",
"short_name": "Your Store",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}…and link it from your <head>:
<link rel="manifest" href="/manifest.webmanifest">
<link rel="apple-touch-icon" href="/icon-192.png">
<meta name="apple-mobile-web-app-capable" content="yes">Set data-ios-hint="true" on the snippet to show iPhone visitors a friendly how-to card automatically.
Prompt options
| Attribute | Default | What it does |
|---|---|---|
data-site | required | Your site key from the dashboard (pk_…). |
data-delay | 6000 | Milliseconds to wait before showing the soft prompt. |
data-scroll | 0 | Also wait until the visitor has scrolled this % of the page (0 = off). |
data-cap-days | 3 | After “Not now”, wait this many days before asking again. |
data-max-dismiss | 3 | Stop asking automatically after this many “Not now” taps. |
data-auto | true | Set to false to never prompt automatically — only from your own button via Pusharoo.subscribe(). |
data-title | “Get updates from {Your site}?” | Prompt headline. |
data-text | friendly default | Prompt body text (or set it per site in the dashboard). |
data-allow / data-later | Allow / Not now | Button labels. |
data-position | auto | auto (top-right on desktop, bottom sheet on phones), top or bottom. |
data-ios-hint | false | true shows iPhone visitors a short “Add to Home Screen” how-to card. |
data-sw | /sw.js | Path of your service worker if it isn’t /sw.js. |
data-debug | false | Log what the snippet is doing to the console. |
Already have a service worker?
A page can only have one service worker per scope, so don’t register a second one. Instead add these two lines at the top of your existing worker and point the snippet at it:
self.PUSHAROO_SITE = 'pk_your_site_key';
importScripts('https://pusharoo.com/sw.js');<script src="https://pusharoo.com/sdk/pusharoo.js" data-site="pk_your_site_key" data-sw="/your-existing-sw.js" async></script>Cloudflare & caching
If your site is behind Cloudflare (or any CDN), make sure /sw.js and your manifest are never cached at the edge, so updates reach visitors right away. In Cloudflare: Caching → Cache Rules → Create rule:
- If: URI Path equals
/sw.jsor URI Path equals/manifest.webmanifest - Then: Cache eligibility → Bypass cache
Also make sure Rocket Loader or other script optimizers don’t rewrite sw.js. The snippet registers the worker with updateViaCache: 'none', so the browser always checks for a fresh copy.
JavaScript API & events
The snippet exposes window.Pusharoo so you can build your own “Get alerts” button or bell:
<button onclick="Pusharoo.subscribe()">🔔 Get deal alerts</button>
Pusharoo.state(); // "default" | "subscribed" | "granted" | "denied" | "unsupported" | "ios-needs-home-screen"
Pusharoo.subscribe(); // shows your soft prompt now (ignores the frequency cap)
Pusharoo.prompt(); // shows the prompt only if the frequency cap allows
Pusharoo.unsubscribe(); // unsubscribes this browserAnd it fires browser events you can hook into (analytics, UI, your CRM):
window.addEventListener('pusharoo:prompt-shown', () => {});
window.addEventListener('pusharoo:subscribed', e => console.log(e.detail.endpoint, e.detail.isNew));
window.addEventListener('pusharoo:dismissed', e => {});
window.addEventListener('pusharoo:permission', e => console.log(e.detail.permission));
window.addEventListener('pusharoo:unsubscribed', () => {});Tags & events
Tell Pusharoo about a subscriber so you can segment and automate on it. Calls made before someone subscribes are queued in the browser and sent when they do.
Pusharoo.tag('plan', 'pro'); // one tag
Pusharoo.tag({ plan: 'pro', city: 'Toronto' }); // several at once (up to 30)
Pusharoo.untag('plan'); // remove
Pusharoo.track('added_to_cart'); // a custom event (letters, numbers, _ . - :)
Pusharoo.track('purchase', { order_id: '1001', value: 49.99 }); // with properties (text, numbers, true/false; up to 25 keys, 2 KB)Use them in Segments (“Tag” and “Did event”), as automation triggers (“A custom event happens”, “A tag is set”), and as a stop condition (“Stop if event: purchase” cancels a waiting cart reminder). Your server can send the same tags and events with the REST API.
Webhooks
Add an HTTPS webhook URL to any website in the dashboard and Pusharoo will POST JSON to it for subscribe, unsubscribe, click (with variant and button) and custom event events. Works with Zapier and Make: paste a “Catch Hook” URL as the webhook. Failed deliveries are retried with backoff (up to 6 attempts).
POST https://your-server.com/pusharoo-hook
X-Pusharoo-Event: subscribe
X-Pusharoo-Timestamp: 1794470000
X-Pusharoo-Signature: sha256=<hex HMAC>
{"event":"subscribe","site":{"id":1,"name":"Your Store","origin":"https://yourstore.com"},
"data":{"subscription_id":42,"platform":"android","locale":"en-CA","timezone":"America/Toronto"},
"ts":"2026-11-12T11:00:00+00:00"}Verify the signature with the webhook secret shown in the dashboard:
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PUSHAROO_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_PUSHAROO_SIGNATURE'] ?? '';
$ok = hash_equals('sha256=' . hash_hmac('sha256', $ts . '.' . $raw, getenv('PUSHAROO_WEBHOOK_SECRET')), $sig)
&& abs(time() - (int)$ts) < 300;Segments, schedules & automations
- Segments — combine website, device (desktop, Android, iPhone/iPad), seen in the last N days, inactive for N+ days, joined in the last N days, language, timezone (e.g.
Europe/), tag (key or key=value), did a custom event, clicked in the last N days, or never clicked. Reach is counted live; save segments to reuse them. - Schedules — send now, pick a date and time, or repeat every day, week or month (optionally until a date). Each repeat is its own campaign with its own stats; “Stop repeating” ends the series.
- Drafts — “Save draft” keeps a campaign for later; RSS feeds can create drafts for you to review.
- Reliable sending — campaigns send in the background and save progress after every batch. If the sender is interrupted, it picks up where it stopped without sending anyone a duplicate. “Stop” halts a send mid-way; “Send again” resumes it.
- Automations — trigger on: someone subscribes, a custom event, a tag being set, a click on any notification, or a subscriber going quiet for N days (re-engagement). Add a wait (minutes, hours, days) and an optional “stop if event” — e.g. added_to_cart → wait 1 hour → “You left something behind”, stop if purchase.
- Stats — every campaign and automation shows targeted, delivered, expired (cleaned up), clicks and CTR; A/B tests show both variants side by side.
- Subscriber tags — the Subscribers page shows each person’s tags; click a tag to filter by it, × to remove it, + to add one by hand (handy for VIPs and testers).
Local time, quiet hours & frequency caps
Every website has delivery rules (Websites → your site → Delivery rules) with sensible defaults, so nobody gets buzzed at 3 AM or five times a day:
| Rule | Default | What it does |
|---|---|---|
| Subscriber’s timezone | from their browser | Captured at subscribe time; the website’s fallback timezone (default Toronto) covers anyone without one. |
| Quiet hours | 10 PM – 8 AM, on | A campaign that would land in someone’s quiet hours waits and goes out when they end — per subscriber, per timezone. Delayed automation messages (wait ≥ 10 min, re-engagement) wait too; instant welcomes don’t. |
| Frequency cap | 4 per 24 hours | People who already got that many campaigns in the window are skipped for this one and logged as capped. 0 turns the cap off. |
Send in local time: schedule a campaign and tick Subscriber’s local time — everyone gets it at that clock time where they are (9 AM in Halifax, then 9 AM in Toronto, then Vancouver…). Repeating campaigns keep the same local time every day, week or month. Urgent campaigns ignore quiet hours and caps — keep it for genuinely time-critical alerts.
Waiting and resumed sends never duplicate: each subscriber gets a campaign at most once, however many passes it takes. The campaign page shows how many are still waiting and how many the cap skipped. Waiting subscribers are delivered within 26 hours (50 for local-time sends); anyone unreachable after that is reported, not retried forever.
A/B tests & action buttons
A/B tests: in a new campaign, write a variant B title, message or image, then choose 50/50 split (everyone gets A or B) or Test on a slice, then send the winner (10–50% get A or B; after the wait you choose, the variant with the higher click-through rate goes to everyone else automatically). Nobody receives both.
Action buttons: add up to two buttons, each with its own link (e.g. “Shop now” and “See details”). Chrome, Edge and Android show them; other browsers show the notification without buttons and the main link still works. Button clicks are counted separately. Automation messages (welcome, abandoned cart, re-engagement…) can have buttons too.
Results: the campaign page charts click-through rate for A and B side by side, marks the leader/winner, and tells you whether the gap is statistically significant (two-proportion test at 90%/95%) or still too close to call.
RSS-to-push
Dashboard → Integrations → Add a feed. Paste any RSS or Atom feed (WordPress /feed/, Ghost, Substack, Shopify blog .atom, YouTube channel feeds), pick the website and, optionally, a tag/event audience. New posts become notifications automatically — or drafts for you to review. Title = post title, message = summary, image = featured image, click = the post. Only posts published after you add the feed are sent, at most one per check, so a backlog never floods anyone.
REST API
Create a key in Integrations → REST API. It’s shown once and stored hashed. The API is server-to-server only (requests with a browser Origin are refused), limited to 120 requests a minute per key, and only sees your own websites.
curl https://pusharoo.com/api/v1/campaigns \
-H "Authorization: Bearer $PUSHAROO_KEY" -H "Content-Type: application/json" \
-d '{"title":"Back in stock","body":"Your size is here","url":"https://yourstore.com/p/1",
"site_id":1,"filters":{"tag_key":"wishlist","tag_value":"p1"},
"buttons":[{"label":"Buy now","url":"https://yourstore.com/cart/add/1"}],
"local_time":"2026-12-01T09:00"}'| Method & path | What it does |
|---|---|
GET /api/v1/me | Check the key |
GET /api/v1/sites | Your websites, site keys and active subscriber counts |
GET /api/v1/segments | Saved segments with live counts |
POST /api/v1/audience | Count an audience: site_id, segment_id, filters |
POST /api/v1/campaigns | Send now, schedule (send_at), or "draft":true. Optional icon, image, buttons, segment_id, filters, subscriber_ids, repeat (daily/weekly/monthly), ab {mode: split|winner, title, body, image, test_pct, wait_minutes}, local_time ("2026-12-01T09:00" — that wall-clock time in each subscriber’s timezone), urgent (skip quiet hours and caps) |
GET /api/v1/campaigns, GET /api/v1/campaigns/ | Status and stats (incl. A/B results) |
POST /api/v1/campaigns/ · /cancel | Send a draft (or resume) · cancel / stop |
GET /api/v1/subscribers | Paginate with after + limit (≤500); filter with site_id, tag_key, tag_value, event… |
POST /api/v1/subscribers/ | {"tags":{"plan":"pro","trial":null}} — null removes |
POST /api/v1/events | {"subscriber_id":42,"event":"purchase","properties":{"order_id":"1001","value":49.99},"idempotency_key":"order-1001"} — fires event flows and automations, wakes “wait until event” steps, and stops flows that cancel on it. Retries with the same idempotency_key are recorded once. |
GET /api/v1/events?subscriber_id=42 | Recent events for a subscriber, with their properties |
POST /api/v1/notify | Transactional: send immediately to up to 1,000 subscriber_ids (“your order shipped”) |
GET /api/v1/stats | Active subscribers, new this week, delivered and clicks in 30 days |
POST /api/v1/hooks | Subscribe a URL to an event (subscribe, unsubscribe, click, event, campaign_sent); returns id and a signing secret |
GET /api/v1/hooks · DELETE /api/v1/hooks/{id} | List · remove hook subscriptions |
GET /api/v1/hooks/sample?event=… | A sample payload (array) for field mapping in Zapier/Make |
Get a subscriber’s id from the pusharoo:subscribed browser event’s endpoint via your webhook (subscribe carries subscription_id), then store it on your user record.
Zapier, Make & n8n
Pusharoo works with Zapier, Make, n8n and Pipedream today through their built-in webhook and HTTP tools — no app install needed. Create an API key in Integrations first.
Triggers — “when something happens in Pusharoo”
- Zapier: add Webhooks by Zapier → Catch Hook and copy its URL. Make: add Webhooks → Custom webhook and copy its address. n8n: a Webhook node’s production URL.
- Subscribe that URL to an event (run once from a terminal, or from a Zapier “Code”/Make “HTTP” step):
curl https://pusharoo.com/api/v1/hooks \
-H "Authorization: Bearer $PUSHAROO_KEY" -H "Content-Type: application/json" \
-d '{"event":"subscribe","target_url":"https://hooks.zapier.com/hooks/catch/123/abc/","site_id":1}'Events arrive within about a minute as JSON: id, event, site_id, site_name, site_origin, created_at plus subscription_id, platform, locale, timezone (subscribe) · campaign_id, variant, button (click) · event_name (event) · title, targeted, delivered, clicks (campaign_sent). Each request carries X-Pusharoo-Timestamp and X-Pusharoo-Signature: sha256=HMAC(secret, timestamp + "." + body). Failed deliveries retry with backoff (6 tries); answering 410 Gone removes the hook. Connected hooks are listed (and removable) under Integrations.
Actions — “send a push when something happens elsewhere”
Add Webhooks by Zapier → POST (Make: HTTP → Make a request) with header Authorization: Bearer YOUR_KEY, JSON body:
| Recipe | URL | Body |
|---|---|---|
| New blog post / product → push to everyone | /api/v1/campaigns | {"title":"…","body":"…","url":"…","site_id":1} |
| Order shipped → push to that customer | /api/v1/notify | {"subscriber_ids":[42],"title":"Your order shipped","body":"…","url":"…"} |
| Purchase → stop abandoned-cart automation + tag | /api/v1/events | {"subscriber_id":42,"event":"purchase","tags":{"customer":"yes"}} |
| New subscriber → Google Sheets / HubSpot / Slack | Trigger subscribe → any app’s “create row / contact / message” action | |
| Campaign finished → Slack report | Trigger campaign_sent → Slack “send message” with delivered and clicks | |
Store the subscriber’s subscription_id (from the subscribe trigger) on your customer record so later steps can target them. Native Zapier and Make apps with these same triggers and actions are built and waiting on directory review.
Google Tag Manager
Create a Custom HTML tag with the snippet, trigger All Pages, publish. You still need /sw.js at your site root (download the two-line version from your website page in the dashboard). Tag and track from other tags with Pusharoo.tag() / Pusharoo.track().
WordPress
- Upload
sw.jsto the folder that containswp-config.php(your site root) with your host’s File Manager or SFTP. Check thathttps://yoursite.com/sw.jsopens. - Add the snippet to your header: in a block theme use Appearance → Editor → Patterns/Template parts → Header with a Custom HTML block, or use any “insert headers and footers” plugin, or add it to your child theme’s
header.phpbefore</head>. - Caching plugins (WP Rocket, LiteSpeed Cache, W3TC): exclude
/sw.jsfrom caching and from JS minify/combine, and excludepusharoo.jsfrom “delay JavaScript”.
Shopify & hosted site builders
yourstore.com/sw.js, and browsers only accept a service worker from your own domain. A free Cloudflare Worker fixes that in about five minutes. Check my installation detects these platforms and points you here.Shopify, step by step
- Put the domain on Cloudflare. Add your store domain to Cloudflare (free plan), keep Shopify’s DNS records, and make sure the record for your store (
wwwor the root) is proxied (orange cloud). Set SSL/TLS to Full. Shopify supports this setup; check the domain still loads with no redirect loop. - Download your files. Pusharoo → Websites → your store → download Cloudflare Worker and Shopify theme snippet. Both have your site key built in.
- Deploy the Worker. Cloudflare → Workers & Pages → Create → Worker → paste the Worker file → Deploy. Then Settings → Domains & Routes → Add route:
yourstore.com/sw.js*andyourstore.com/manifest.webmanifest*(zone = your domain). Openhttps://yourstore.com/sw.js— you should see two Pusharoo lines. - Add the snippet to your theme. Shopify admin → Online Store → Themes → … → Edit code → Snippets → Add a new snippet named
pusharoo→ paste the theme snippet → save. Openlayout/theme.liquidand add{% render 'pusharoo' %}just before</head>. Save. - Check. Pusharoo → your website → Check my installation, then open the store and tap Allow.
No theme edits? Set INJECT_SNIPPET = true at the top of the Worker and use the route yourstore.com/* instead: the Worker adds the snippet and manifest link to every page itself (skipping pages that already have it) and passes everything else through untouched.
Already have a service worker? The Worker keeps it: with MERGE_EXISTING = true (default) it fetches your existing /sw.js and adds Pusharoo on top, so a PWA or another push tool keeps working.
Customer events: add Pusharoo.tag('customer', 'yes') inside {% if customer %} in the snippet, and Pusharoo.track('added_to_cart') on your cart button, to power abandoned-cart and VIP automations. Shopify Flow or Zapier can call /api/v1/events on purchase to stop the abandoned-cart message.
Wix, Squarespace, Webflow
Same Worker and routes. Paste the snippet into the builder’s custom-code area (Wix: Settings → Custom code → Head, all pages · Squarespace: Settings → Advanced → Code injection → Header · Webflow: Site settings → Custom code → Head code), or set INJECT_SNIPPET = true.
Not on Cloudflare?
Collect subscribers on a site you control (a blog or landing page on your own hosting) and link to the store from your notifications. A one-click Shopify app (app embed, no Cloudflare) is planned; it needs a Shopify Partner listing.
Test checklist
https://yoursite.com/sw.jsopens and shows JavaScript.- Open your site in Chrome desktop, wait for the prompt, tap Allow twice.
- In the dashboard, your website’s subscriber count goes up by one.
- Send a campaign (or use “Send test” on a subscriber) and watch it arrive.
- On Android Chrome, repeat. On iPhone, Add to Home Screen first.
Troubleshooting
- No prompt? You may have tapped “Not now” recently or blocked notifications. Try a private window, or run
localStorage.clear()in the console, or reset the site’s notification permission in browser settings. Adddata-debug="true"to see logs. - “origin not allowed” — the website address in the dashboard must exactly match the page’s origin (https, same domain).
- Subscribed but nothing arrives — check Focus/Do Not Disturb, OS notification settings for the browser, and that
sw.jsisn’t cached or blocked. - iPhone shows nothing — it must be opened from the Home Screen icon on iOS 16.4+.
Privacy & unsubscribing
Pusharoo stores only the browser’s push address and keys, browser type, language and timezone — no names, emails or cookies. Visitors can unsubscribe from the browser’s site settings at any time, or via your own button using Pusharoo.unsubscribe(). Addresses that browsers report as gone (HTTP 404/410) are removed from sending immediately and purged automatically. See our privacy note.
Uninstall
Remove the script tag. If you also want existing subscribers’ browsers to drop the worker, replace /sw.js with this for a few weeks before deleting it:
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.registration.unregister());