Receive real-time notifications when events occur in Pivot using webhooks.
Webhooks allow you to receive real-time notifications when events occur in Pivot. When an event happens, Pivot sends an HTTP POST request to your configured endpoint with details about the event.
To create a webhook, send a POST request to the webhooks endpoint:
POST /v1/organizations/{organization_id}/webhooksAuthorization: Bearer your_api_keyContent-Type: application/json
{ "content": { "name": "My Webhook", "description": "Notifications for room messages", "endpoint_url": "https://your-server.com/webhook", "subscriptions": [ { "subject_type": "WEBHOOK_SUBJECT_TYPE_ROOM", "subject_id": "<room_uuid>", "subscription_type": "WEBHOOK_SUBSCRIPTION_TYPE_MESSAGE_SENT", "filter": { "room_type": "chat" } } ] }}The response includes the webhook details and a secret for signature verification:
{ "webhook": { "id": "webhook-uuid", "organization_id": "org-uuid", "name": "My Webhook", "endpoint_url": "https://your-server.com/webhook", "status": "WEBHOOK_STATUS_ACTIVE", "subscriptions": [...] }, "secret": "your-webhook-secret"}Important: Store the secret securely. It cannot be retrieved again and is required to verify webhook signatures.
Each webhook retains the secret it was created with (which may be shared by other webhooks in the organization).
The following event types are available for webhook subscriptions:
| Subscription Type | Event Type | Description |
|---|---|---|
WEBHOOK_SUBSCRIPTION_TYPE_MESSAGE_SENT | message_sent | A message was sent in a room |
WEBHOOK_SUBSCRIPTION_TYPE_MESSAGE_DELETED | message_deleted | A message was deleted in a room |
WEBHOOK_SUBSCRIPTION_TYPE_MESSAGE_EDITED | message_edited | A message was edited in a room |
WEBHOOK_SUBSCRIPTION_TYPE_ROOM_RECORDING_PUBLISHED | room_recording_published | A recording video is ready |
WEBHOOK_SUBSCRIPTION_TYPE_ROOM_RECORDING_TRANSCRIPT_PUBLISHED | room_recording_transcript_published | A recording transcript is ready |
WEBHOOK_SUBSCRIPTION_TYPE_BLOCK_RESPONSE_SENT | block_response_sent | A block response was submitted |
All webhook payloads use camelCase field names. For complete payload schemas and JSON examples for each event type, see the Webhook Event Reference.
Subscriptions define what entities trigger webhook notifications:
| Subject Type | Description |
|---|---|
WEBHOOK_SUBJECT_TYPE_ROOM | Events for a specific room |
WEBHOOK_SUBJECT_TYPE_SPACE | Events for all rooms in a space |
WEBHOOK_SUBJECT_TYPE_BLOCK | Events for a specific block |
Optionally filter events using the filter object:
{ "subject_type": "WEBHOOK_SUBJECT_TYPE_ROOM", "subject_id": "room-uuid", "subscription_type": "WEBHOOK_SUBSCRIPTION_TYPE_MESSAGE_SENT", "filter": { "room_type": "chat" }}Available filter options:
room_type: Filter by room type (e.g., “chat”, “post”, “video”)All webhook payloads include security headers:
| Header | Description |
|---|---|
X-Pivot-Signature | HMAC-SHA256 signature of the payload |
X-Pivot-Event | The event type (e.g., “message_sent”) |
X-Pivot-Delivery | Unique delivery identifier |
X-Pivot-Webhook-Id | The ID of the webhook that triggered the delivery |
If you’re using the Pivot SDK, signature verification is built in:
import express from 'express';import { parseWebhookEvent } from '@hellopivot/sdk';
// Use express.raw() so req.body is a Buffer (not a parsed object).// The raw string is needed for HMAC signature verification.app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { try { const payload = req.body.toString('utf8'); const event = parseWebhookEvent( payload, req.headers['x-pivot-signature'], process.env.WEBHOOK_SECRET );
// event is fully typed — use event.eventType to handle each case console.log(event.eventType, event.data); res.status(200).send('OK'); } catch { res.status(401).send('Invalid signature'); }});If you’re not using the SDK, compute the HMAC-SHA256 manually:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(payload, signature, secret) { const expected = 'sha256=' + createHmac('sha256', secret).update(payload).digest('hex');
return ( Buffer.byteLength(signature) === Buffer.byteLength(expected) && timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) );}POST /v1/organizations/{organization_id}/webhooks/secret/rotateRotateWebhookSecretRequest uses only the organization_id path parameter.
No request body is required. The response returns the new secret used for future webhooks.
Use this when you want newly created webhooks to use a new signing secret. Existing webhooks keep their current secret and are not changed by this operation.
GET /v1/organizations/{organization_id}/webhooksOptionally filter by webhook name (case-insensitive substring match):
GET /v1/organizations/{organization_id}/webhooks?name=My%20WebhookPATCH /v1/organizations/{organization_id}/webhooks/{webhook_id}
{ "content": { "name": "Updated Name", "status": "WEBHOOK_STATUS_INACTIVE" }}DELETE /v1/organizations/{organization_id}/webhooks/{webhook_id}GET /v1/organizations/{organization_id}/webhooks/{webhook_id}/logsLogs are retained for 30 days and include delivery status, response codes, and retry counts.
X-Pivot-Delivery header to detect duplicate deliveriesFailed webhook deliveries are retried up to 5 times with exponential backoff:
| Retry | Delay |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 24 hours |
After all retries are exhausted, organization admins receive an email notification about the failed webhook.
To protect your system and ours, webhooks are automatically paused if they experience sustained failures:
WEBHOOK_STATUS_ACTIVE to WEBHOOK_STATUS_PAUSEDWhen a webhook is paused:
To reactivate a paused webhook, update its status to active:
PATCH /v1/organizations/{organization_id}/webhooks/{webhook_id}
{ "content": { "status": "WEBHOOK_STATUS_ACTIVE" }}Before reactivating, ensure your endpoint is functioning correctly to avoid the webhook being paused again.
| Action | Required Role |
|---|---|
| List webhooks | Organization reader role |
| Create/Update/Delete webhooks | Organization admin or super_admin |
When creating subscriptions, you must be an organization admin (or super admin). Subject-specific room, block, or space access is not required.
Was this guide helpful?