Skip to main content

Events API

POST /v1/e/:kind/:msgId

No authentication, no body. Rate-limited per client IP (EVENT_RATE_LIMIT).

kindEvent
ddelivered
oopened
cclicked
xdismissed

msgId is the tracking id carried in every push (on.m on APNs, data.msgId on FCM and web). It is

base64url("appId|campaignId|userId|subscriptionId|platform|variant") + "." + hmac-sha256(HMAC_SECRET)[0:16]

so the API can verify it and know everything it needs without a database read. On success it writes the event, increments stats.<type> (and stats.variants.<variant>.<type>) on the campaign, emits notification.<type> to webhooks, and answers 204.

curl -X POST https://push.example.com/v1/e/o/eyJhcHBJZCI6… # → 204

Errors: 400 unknown_kind, 400 invalid_msg_id (signature did not verify — a tampered or foreign id), 429 rate_limited, 503 database_unavailable.

Security model

Whoever received the push can replay its msgId. What that buys them is moving an analytics counter on their own campaign — nothing more. No data is returned, nothing is granted, and the per-IP cap bounds the noise. Rotating HMAC_SECRET invalidates ids already in flight, so do it right after a campaign has settled.

Custom clients

If you build your own client instead of using an SDK, call this endpoint with keepalive: true / a short timeout and ignore failures — a tracking ping must never delay showing the notification. The iOS extension has ~30 s of runtime and uses a 5 s timeout; copy that.