Events API
POST /v1/e/:kind/:msgId
No authentication, no body. Rate-limited per client IP (EVENT_RATE_LIMIT).
kind | Event |
|---|---|
d | delivered |
o | opened |
c | clicked |
x | dismissed |
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.