Developer guide
One snippet for websites, four calls for everything else. Nothing has to be declared first: send an event or a property, and it appears in filters by itself.
Websites
Your snippet and public key are under Settings. Paste it before </head>:
<script>window.engagemage=window.engagemage||function(){var q=[],o={q:q};["track","identify","reset","enablePush"].forEach(function(m){o[m]=function(){q.push([m,arguments])}});return o}();</script>
<script src="https://harrowsoft.com/engagemage/sdk.js" data-key="pk_your_public_key" async></script>
The first line lets your own code call engagemage straight away, before the file has finished loading. If your site has a content security policy, allow harrowsoft.com for scripts and connections.
Visitors get an anonymous ID automatically. Once you know who someone is, say so, and their earlier activity joins their record:
engagemage.identify("user_123", { email: "ava@example.com", first_name: "Ava", plan: "pro" });
engagemage.track("Purchased", { amount: 24, product: "Ridgeline" });
engagemage.reset(); // when they sign out
The snippet also shows any in-app messages waiting for that person. There's nothing else to install.
Servers
Create a secret key in Settings. It belongs on your server only. Send one item, or up to 1,000 as {"batch": [...]}.
POST /v1/track
curl https://harrowsoft.com/engagemage/v1/track \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"user_id":"user_123","event":"Purchased","properties":{"amount":24},"ts":"2026-11-02T10:00:00Z"}'
ts is optional, and can be an ISO date or Unix time. Events more than an hour old count in filters and reports but don't start campaigns. Add your own event_id if your code retries: sending the same ID twice counts once.
POST /v1/identify
curl https://harrowsoft.com/engagemage/v1/identify \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"user_id":"user_123","email":"ava@example.com","phone":"+14155550100","tz":"America/Chicago","attributes":{"first_name":"Ava","plan":"pro"}}'
You can identify someone by user_id, email or anon_id. Setting an attribute to null removes it.
Mobile apps
Call the same endpoints with your public key. Keep one random anon_id per install until the person signs in.
// Swift
var req = URLRequest(url: URL(string: "https://harrowsoft.com/engagemage/v1/track")!)
req.httpMethod = "POST"
req.setValue("Bearer pk_your_public_key", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: ["user_id": userId, "event": "Opened App"])
URLSession.shared.dataTask(with: req).resume()
To show in-app messages, fetch them when the app opens, and report back what happened:
POST /v1/inbox {"user_id":"user_123"}
→ {"messages":[{"token":"...","content":{"style":"modal","title":"...","body":"...","button":"...","url":"..."}}]}
POST /v1/inbox/ack {"token":"...","action":"shown"} // or "clicked"
Push
On the web
Save this one line as engagemage-sw.js at the root of your site:
importScripts("https://harrowsoft.com/engagemage/sw.js");
Then ask for permission at a moment that makes sense. Browsers require a click.
button.onclick = () => engagemage.enablePush();
On phones
Connect your Firebase project under Settings, then register each device when your app gets its token. This covers Android, and iOS through Firebase.
POST /v1/push/subscribe {"user_id":"user_123","fcm_token":"DEVICE_TOKEN"}
Devices that uninstall or turn notifications off are forgotten automatically.
Starting a campaign from your server
Set a campaign's "When?" to "From my server". This suits receipts, password resets and alerts. Needs a secret key.
curl https://harrowsoft.com/engagemage/v1/trigger \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"campaign_id":12,"user_id":"user_123","data":{"order":"A-1001"}}'
Use data in the message as {{event.order}}. Campaigns marked transactional ignore the daily limit, quiet hours and unsubscribes, so use that only for messages people need.
Shopify, Stripe and bounces
These need no code. Settings gives you an address to paste into each service. Orders and payments become events with their amounts, and bounces and spam complaints stop further email to that address. Each address is private to your workspace.
Deleting someone
When someone asks to be forgotten, this removes them and everything recorded about them. Needs a secret key.
POST /v1/delete {"user_id":"user_123"} // or {"email":"ava@example.com"}
Signed IDs
Your public key is visible in your site's code, so it can only send data in. By default, a browser call can fill in a missing email or phone but can't replace one. If your in-app messages contain anything private, turn on "Require signed user IDs" in Settings, then prove each user on your server:
// Node
const hash = crypto.createHmac("sha256", process.env.ENGAGEMAGE_IDENTITY_SECRET).update(userId).digest("hex");
# Python
hash = hmac.new(SECRET.encode(), user_id.encode(), hashlib.sha256).hexdigest()
// In the page
engagemage.identify(userId, { first_name: "Ava" }, { userHash: hash });
In direct API calls, send it as user_hash. Anonymous visitors and secret-key calls don't need one.
Names that do something
- In attributes:
email,phoneandtzare how we reach someone.first_nameis what templates usually start with. - In event properties:
value,amount,revenue,totalorpricecounts as money in campaign results, with no setup. - In messages:
{{first_name|there}}uses "there" if the person has no first name.{{event.product}}reads from the event that started the campaign.
Limits and errors
- Up to 1,000 items per batch, 2 MB per request, and 5,000 requests a second per workspace.
202means accepted. Errors are JSON with a plain-Englisherror.401bad key.403this key can't do that, or a signed ID is required.429slow down.507storage limit reached.