EngageMage

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

Limits and errors