Webhooks
UserGist sends outbound webhooks for push delivery transitions. Every payload
is HMAC-signed; verify it before trusting the body. Managing endpoints requires
a signed-in dashboard operator with the admin role.
Subscribe
POST /v1/apps/:appId/push/webhooks
{
"url": "https://hooks.acme.com/usergist",
"eventTypes": ["delivered", "opened", "clicked"],
"active": true,
"description": "Production delivery events"
}The endpoint must use HTTPS and pass UserGist's public-network URL validation.
The response generates a 256-bit secret; store it in your secrets manager
because it is returned only on creation or explicit rotation.
Payload
POST https://hooks.acme.com/usergist HTTP/1.1
Content-Type: application/json
X-UserGist-Event: opened
X-UserGist-Signature: t=1716559200,v1=4f8d...
{
"event": "opened",
"app_id": "app_123",
"fired_at": "2026-05-24T10:00:00Z",
"data": {
"campaignId": "cmp_abc",
"deliveryId": "del_abcdef",
"platform": "ios",
"openedAt": "2026-05-24T10:00:00Z"
}
}Signature verification
The X-UserGist-Signature header is t=<unix_ts>,v1=<signature>. The
signature is the base64url-encoded HMAC-SHA256 of <timestamp>.<raw_body>.
Always verify the exact request bytes before parsing JSON.
import crypto from 'node:crypto'
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const ts = parts.t
const sig = parts.v1
if (!ts || !sig) return false
if (Math.abs(Date.now() / 1000 - Number(ts)) > 5 * 60) return false // 5-min replay window
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest()
const received = Buffer.from(sig, 'base64url')
return expected.length === received.length && crypto.timingSafeEqual(expected, received)
}Reject anything that doesn't verify and skip-process anything older than 5 minutes.
Event types
| Type | Fires on |
|---|---|
sent | Delivery was handed to the provider path. |
delivered | APNs/FCM accepted the payload. |
displayed | A device-side display beacon was received. |
opened | User tapped the notification. |
clicked | User tapped a CTA action button. |
dismissed | User dismissed the notification. |
bounced | The provider reported a permanent delivery problem. |
failed | Delivery failed. |
Retries
UserGist makes at most five attempts for timeouts, network failures, 408,
429, and 5xx responses, using bounded quadratic backoff with jitter. Other
4xx responses are treated as permanent. The endpoint records the most recent
success or failure for operator visibility.
What's next
- Features → Push
- API → Errors
- Guides → Run an NPS survey — Slack-on-detractors example