Skip to content
Setup guide · about 5 minutes

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.

What you need: your site on HTTPS, the ability to place a file at 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:

Download sw.js

Or from a terminal on your server:

curl -o /path/to/your/webroot/sw.js https://pusharoo.com/sw.js

Serve 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:

  1. Tap Share → Add to Home Screen.
  2. Open your site from the new Home Screen icon.
  3. 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

AttributeDefaultWhat it does
data-siterequiredYour site key from the dashboard (pk_…).
data-delay6000Milliseconds to wait before showing the soft prompt.
data-scroll0Also wait until the visitor has scrolled this % of the page (0 = off).
data-cap-days3After “Not now”, wait this many days before asking again.
data-max-dismiss3Stop asking automatically after this many “Not now” taps.
data-autotrueSet to false to never prompt automatically — only from your own button via Pusharoo.subscribe().
data-title“Get updates from {Your site}?”Prompt headline.
data-textfriendly defaultPrompt body text (or set it per site in the dashboard).
data-allow / data-laterAllow / Not nowButton labels.
data-positionautoauto (top-right on desktop, bottom sheet on phones), top or bottom.
data-ios-hintfalsetrue shows iPhone visitors a short “Add to Home Screen” how-to card.
data-sw/sw.jsPath of your service worker if it isn’t /sw.js.
data-debugfalseLog 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:

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 browser

And 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

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:

RuleDefaultWhat it does
Subscriber’s timezonefrom their browserCaptured at subscribe time; the website’s fallback timezone (default Toronto) covers anyone without one.
Quiet hours10 PM – 8 AM, onA 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 cap4 per 24 hoursPeople 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 & pathWhat it does
GET /api/v1/meCheck the key
GET /api/v1/sitesYour websites, site keys and active subscriber counts
GET /api/v1/segmentsSaved segments with live counts
POST /api/v1/audienceCount an audience: site_id, segment_id, filters
POST /api/v1/campaignsSend 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//send · /cancelSend a draft (or resume) · cancel / stop
GET /api/v1/subscribersPaginate with after + limit (≤500); filter with site_id, tag_key, tag_value, event…
POST /api/v1/subscribers//tags{"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=42Recent events for a subscriber, with their properties
POST /api/v1/notifyTransactional: send immediately to up to 1,000 subscriber_ids (“your order shipped”)
GET /api/v1/statsActive subscribers, new this week, delivered and clicks in 30 days
POST /api/v1/hooksSubscribe 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”

  1. 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.
  2. 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:

RecipeURLBody
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 / SlackTrigger subscribe → any app’s “create row / contact / message” action
Campaign finished → Slack reportTrigger 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

  1. Upload sw.js to the folder that contains wp-config.php (your site root) with your host’s File Manager or SFTP. Check that https://yoursite.com/sw.js opens.
  2. 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.php before </head>.
  3. Caching plugins (WP Rocket, LiteSpeed Cache, W3TC): exclude /sw.js from caching and from JS minify/combine, and exclude pusharoo.js from “delay JavaScript”.

Shopify & hosted site builders

Root file limitation: Shopify, Wix, Squarespace and Webflow don’t let you upload a file that opens at 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

  1. 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 (www or the root) is proxied (orange cloud). Set SSL/TLS to Full. Shopify supports this setup; check the domain still loads with no redirect loop.
  2. Download your files. Pusharoo → Websites → your store → download Cloudflare Worker and Shopify theme snippet. Both have your site key built in.
  3. Deploy the Worker. Cloudflare → Workers & Pages → Create → Worker → paste the Worker file → Deploy. Then Settings → Domains & Routes → Add route: yourstore.com/sw.js* and yourstore.com/manifest.webmanifest* (zone = your domain). Open https://yourstore.com/sw.js — you should see two Pusharoo lines.
  4. 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. Open layout/theme.liquid and add {% render 'pusharoo' %} just before </head>. Save.
  5. 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

  1. https://yoursite.com/sw.js opens and shows JavaScript.
  2. Open your site in Chrome desktop, wait for the prompt, tap Allow twice.
  3. In the dashboard, your website’s subscriber count goes up by one.
  4. Send a campaign (or use “Send test” on a subscriber) and watch it arrive.
  5. On Android Chrome, repeat. On iPhone, Add to Home Screen first.

Troubleshooting

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());