This document was written by AI and has been manually reviewed.
App Notifications
Prism can notify your OAuth application whenever certain events occur — for example, when a user grants or revokes your app's access, or when a user's profile changes.
Three delivery channels are available:
| Channel | Best for |
|---|---|
| Webhook | Server-to-server push; fire-and-forget per event |
| SSE | Server-side streaming (Node.js, Bun, Workers) |
| WebSocket | Bidirectional; works in browsers too |
All channels share the same event types and payload format.
Authentication
SSE and WebSocket connections authenticate with your app's credentials using HTTP Basic auth (client_id:client_secret).
For browser WebSocket (where setting headers is not possible), pass the credentials as query parameters instead:
?client_id=<clientId>&client_secret=<clientSecret>Webhook management endpoints require a user Bearer token with write access to the app.
Events
| Event type | Triggered when |
|---|---|
user.token_granted | A user completes the OAuth consent flow and grants your app access |
user.token_revoked | A user revokes your app's access from their settings |
user.updated | A user who has granted your app access updates their profile |
* | Wildcard — subscribe to all of the above |
Payload format
Every event (webhook body, SSE data: line, or WebSocket message) is a JSON object:
{
"event": "user.token_granted",
"timestamp": 1741564800,
"data": { ... }
}user.token_granted
{
"event": "user.token_granted",
"timestamp": 1741564800,
"data": {
"user_id": "usr_abc123",
"scopes": ["openid", "profile", "email"],
"granted_at": 1741564800
}
}user.token_revoked
{
"event": "user.token_revoked",
"timestamp": 1741564800,
"data": {
"user_id": "usr_abc123"
}
}user.updated
{
"event": "user.updated",
"timestamp": 1741564800,
"data": {
"user_id": "usr_abc123",
"username": "alice",
"display_name": "Alice"
}
}Webhooks
Webhooks are registered on a per-app basis. When an event fires, Prism sends a signed POST request to each active webhook URL subscribed to that event type.
API endpoints
GET /api/apps/:appId/webhooks
POST /api/apps/:appId/webhooks
PATCH /api/apps/:appId/webhooks/:webhookId
DELETE /api/apps/:appId/webhooks/:webhookId
POST /api/apps/:appId/webhooks/:webhookId/test
GET /api/apps/:appId/webhooks/:webhookId/deliveriesAll endpoints require Authorization: Bearer <user-token> where the token owner has write access to the app.
Create a webhook
POST /api/apps/:appId/webhooks
{
"url": "https://example.com/hooks/prism",
"events": ["user.token_granted", "user.token_revoked"],
"secret": "optional-custom-secret"
}If secret is omitted, Prism auto-generates a 32-byte random hex string. The secret is returned only in the creation response — store it securely.
Response:
{
"id": "wh_xyz789",
"app_id": "app_abc123",
"url": "https://example.com/hooks/prism",
"secret": "prism-generated-or-supplied-secret",
"events": ["user.token_granted", "user.token_revoked"],
"is_active": true,
"created_at": 1741564800,
"updated_at": 1741564800
}Verifying the signature
Every webhook request includes an X-Prism-Signature header:
X-Prism-Signature: sha256=<hex-digest>
X-Prism-Event: user.token_granted
X-Prism-Delivery: <uuid>The digest is HMAC-SHA256 of the raw request body using your webhook secret.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(secret, rawBody, signatureHeader) {
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}Always use a timing-safe comparison to prevent timing attacks. Return 2xx to acknowledge receipt; any other status is logged as a failure.
Delivery history
GET /api/apps/:appId/webhooks/:webhookId/deliveries returns the last 50 deliveries:
{
"deliveries": [
{
"id": "del_001",
"webhook_id": "wh_xyz789",
"event_type": "user.token_granted",
"response_status": 200,
"success": true,
"delivered_at": 1741564800
}
]
}Test ping
POST /api/apps/:appId/webhooks/:webhookId/test
Sends a synthetic ping event immediately.
{ "success": true, "status": 200 }Deleting a webhook
DELETE /api/apps/:appId/webhooks/:webhookId
Right before the webhook is removed, Prism delivers one final webhook.deleted event to its URL, so the receiving endpoint can learn that it was deleted. This farewell delivery is sent regardless of the webhook's event subscription and is skipped only when the webhook is inactive. The payload looks like:
{
"event": "webhook.deleted",
"timestamp": 1741564800,
"data": { "id": "wh_xyz789", "app_id": "app_abc123" }
}The delivery is signed with the same X-Prism-Signature scheme as every other webhook event.
Server-Sent Events (SSE)
GET /api/apps/:appId/events/sseOpens a persistent SSE stream. The server polls for new events every 2 seconds and flushes them as they arrive.
Authentication — HTTP Basic header:
Authorization: Basic <base64(clientId:clientSecret)>Or as query parameters (useful when browser EventSource cannot set headers):
?client_id=<clientId>&client_secret=<clientSecret>Resuming — pass the Last-Event-ID header (or ?lastEventId=) to resume from a known event ID and avoid missing events after reconnection.
Each SSE frame:
id: 42
event: user.token_granted
data: {"event":"user.token_granted","timestamp":1741564800,"data":{"user_id":"usr_abc123","scopes":["openid","profile"],"granted_at":1741564800}}WebSocket
GET /api/apps/:appId/events/wsUpgrade to a WebSocket connection. The server pushes events as JSON text frames.
Authentication — HTTP Basic header or query parameters (same as SSE).
Each message frame:
{
"event": "user.token_granted",
"timestamp": 1741564800,
"data": {
"user_id": "usr_abc123",
"scopes": ["openid", "profile"],
"granted_at": 1741564800
}
}Browser example
const ws = new WebSocket(
`wss://your-prism.example/api/apps/${appId}/events/ws` +
`?client_id=${clientId}&client_secret=${clientSecret}`,
);
ws.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
console.log(msg.event, msg.data);
});Security recommendations
- Keep your
client_secretserver-side. Never expose it in browser JavaScript or mobile app binaries. - Always verify webhook signatures before processing payloads.
- Respond to webhooks within 10 seconds; Prism times out after 10 s.
- Use
Last-Event-ID/lastEventIdwhen reconnecting SSE to avoid missing events. - Rotate your
client_secretif it is ever exposed.