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

TypeFires on
sentDelivery was handed to the provider path.
deliveredAPNs/FCM accepted the payload.
displayedA device-side display beacon was received.
openedUser tapped the notification.
clickedUser tapped a CTA action button.
dismissedUser dismissed the notification.
bouncedThe provider reported a permanent delivery problem.
failedDelivery 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