Webhooks
Register and consume webhooks to integrate your SonicJS application with external services. Webhooks notify your app in real time when content is created, workflows transition, users are added, and more.
Overview
SonicJS webhooks are part of the workflow plugin and are powered by the WebhookService. When an event occurs inside SonicJS — such as content being published or a workflow state changing — an HTTP POST request is sent to every active webhook that is subscribed to that event.
Key characteristics:
- Event-driven — webhooks fire automatically when subscribed events occur.
- Signed payloads — each delivery can include an HMAC-SHA256 signature for verification.
- Automatic retries — failed deliveries are retried with exponential backoff.
- Delivery log — every attempt is recorded in the
webhook_deliveriestable for auditing.
Registering webhooks
Webhooks are registered programmatically through the WebhookService. You provide a name, a destination URL, the events you want to subscribe to, and an optional secret for signature verification.
Creating a webhook
import { WebhookService } from '@sonicjs-cms/core'
// The service requires a D1 database binding
const webhookService = new WebhookService(env.DB)
const webhookId = await webhookService.createWebhook(
'My Integration', // name
'https://example.com/webhooks/sonicjs', // url
['content.created', 'content.published'], // events
'whsec_your_secret_key', // secret (optional)
3, // retry count (default: 3)
30 // timeout in seconds (default: 30)
)
Updating a webhook
You can update any property of an existing webhook — its URL, subscribed events, active status, and more.
Updating a webhook
await webhookService.updateWebhook(webhookId, {
events: ['content.created', 'content.updated', 'content.published'],
is_active: true,
retry_count: 5,
})
Listing and deleting webhooks
Managing webhooks
// List all webhooks
const allWebhooks = await webhookService.getWebhooks()
// List only active webhooks
const activeWebhooks = await webhookService.getWebhooks(true)
// Get a single webhook by ID
const webhook = await webhookService.getWebhook(webhookId)
// Delete a webhook
await webhookService.deleteWebhook(webhookId)
Consuming webhooks
When SonicJS fires a webhook, it sends an HTTP POST request to your registered URL. The request body is JSON and always includes the event type in the event field.
Example: Express webhook handler
import express from 'express'
const app = express()
app.post('/webhooks/sonicjs', express.json(), (req, res) => {
const { id, event, timestamp, data, webhook_id } = req.body
switch (event) {
case 'content.created':
console.log('New content created:', data.id)
break
case 'content.published':
console.log('Content published:', data.id)
break
case 'workflow.transition':
console.log(`Workflow: ${data.from_state} → ${data.to_state}`)
break
}
// Respond with 2xx to acknowledge receipt
res.status(200).json({ received: true })
})
Always respond with a 2xx status code to acknowledge the webhook. Any non-2xx response is treated as a failure and will trigger a retry.
Event types
SonicJS fires webhooks for content lifecycle events, workflow transitions, and user events.
- Name
content.created- Description
New content was created in any collection.
- Name
content.updated- Description
An existing content entry was modified. The payload includes a
changesobject with the modified fields.
- Name
content.published- Description
Content transitioned to the published state.
- Name
workflow.transition- Description
A content item moved between workflow states (e.g., draft to pending-review). The payload includes
from_stateandto_state.
- Name
user.created- Description
A new user account was created.
Example payload (content.updated):
{
"id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a",
"event": "content.updated",
"timestamp": "2025-03-15T10:30:00.000Z",
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Getting Started with SonicJS",
"collection_id": "blog-posts",
"changes": {
"title": "Updated: Getting Started with SonicJS",
"status": "published"
},
"action": "updated"
},
"webhook_id": "w1x2y3z4-5678-90ab-cdef-1234567890ab"
}
Webhook payload
Every webhook delivery uses a consistent envelope format.
- Name
id- Type
- string
- Description
Unique delivery ID (UUID). Use this to deduplicate deliveries on your end.
- Name
event- Type
- string
- Description
The event type that triggered this webhook (e.g.,
content.created).
- Name
timestamp- Type
- string
- Description
ISO 8601 timestamp of when the event occurred.
- Name
data- Type
- object
- Description
The event-specific payload. Always contains an
idandactionfield, plus any additional data relevant to the event.
- Name
webhook_id- Type
- string
- Description
The ID of the webhook configuration that matched this event.
Example envelope:
{
"id": "delivery-uuid",
"event": "content.created",
"timestamp": "2025-03-15T10:30:00.000Z",
"data": {
"id": "content-uuid",
"title": "My New Post",
"collection_id": "blog-posts",
"action": "created"
},
"webhook_id": "webhook-uuid"
}
Event-specific payloads
Each event type includes different fields in the data object.
content.created / content.published
- Name
data.id- Type
- string
- Description
The content entry ID.
- Name
data.action- Type
- string
- Description
"created"or"published".
The data object also includes all fields from the content entry itself (title, collection_id, custom fields, etc.).
content.updated
Includes the same fields as above, plus:
- Name
data.changes- Type
- object
- Description
An object containing only the fields that were modified.
workflow.transition
- Name
data.content_id- Type
- string
- Description
The content entry that transitioned.
- Name
data.from_state- Type
- string
- Description
The previous workflow state (e.g.,
"draft").
- Name
data.to_state- Type
- string
- Description
The new workflow state (e.g.,
"pending-review").
user.created
- Name
data.id- Type
- string
- Description
The new user's ID.
- Name
data.action- Type
- string
- Description
Always
"created".
The data object also includes user fields (username, email, role, etc.).
Example (workflow.transition):
{
"id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
"event": "workflow.transition",
"timestamp": "2025-03-15T14:00:00.000Z",
"data": {
"content_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"from_state": "pending-review",
"to_state": "approved",
"action": "workflow_transition"
},
"webhook_id": "w1x2y3z4-5678-90ab-cdef-1234567890ab"
}
Delivery and retries
SonicJS automatically retries failed webhook deliveries using exponential backoff. Each webhook has a configurable retry_count (default: 3) and timeout_seconds (default: 30).
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 4 seconds |
| 3 | 8 seconds |
| 4+ | 16+ seconds |
The delay follows the formula 2^attempt seconds. After all retries are exhausted, the delivery is marked as failed and the webhook's failure_count is incremented.
SonicJS tracks last_success_at, last_failure_at, and failure_count on each webhook so you can monitor delivery health programmatically.
Security
To verify that a webhook delivery genuinely came from SonicJS, provide a secret when registering the webhook. SonicJS will then include an X-SonicJS-Signature header on every delivery containing an HMAC-SHA256 hash of the request body.
Request headers
Every webhook delivery includes these headers:
- Name
Content-Type- Type
- string
- Description
Always
application/json.
- Name
User-Agent- Type
- string
- Description
SonicJS-Webhooks/1.0.
- Name
X-SonicJS-Event- Type
- string
- Description
The event type (e.g.,
content.created).
- Name
X-SonicJS-Delivery- Type
- string
- Description
The unique delivery ID.
- Name
X-SonicJS-Timestamp- Type
- string
- Description
ISO 8601 timestamp of the delivery.
- Name
X-SonicJS-Signature- Type
- string
- Description
HMAC-SHA256 signature in the format
sha256=<hex>. Only present when a secret is configured.
Verifying signatures
Verifying a webhook signature
const crypto = require('crypto')
const signature = req.headers['x-sonicjs-signature']
const payload = JSON.stringify(req.body)
const hash =
'sha256=' +
crypto.createHmac('sha256', secret).update(payload).digest('hex')
if (hash === signature) {
// Request is verified
} else {
// Request could not be verified
}
Keep your webhook secret safe. Never commit it to version control or expose it in client-side code. If your secret is compromised, update the webhook with a new secret immediately.
Delivery history
SonicJS stores a record of every webhook delivery attempt in the webhook_deliveries table. You can query delivery history and retry failed deliveries programmatically.
Working with delivery history
const webhookService = new WebhookService(env.DB)
// Get recent deliveries for a specific webhook
const deliveries = await webhookService.getWebhookDeliveries(webhookId, 50)
// Get all recent deliveries across all webhooks
const allDeliveries = await webhookService.getWebhookDeliveries()
// Retry a failed delivery
await webhookService.retryWebhookDelivery(deliveryId)
// Get delivery statistics
const stats = await webhookService.getWebhookStats(webhookId)
// => { total_deliveries, successful_deliveries, failed_deliveries, average_response_time }
// Clean up deliveries older than 30 days
await webhookService.cleanupOldDeliveries(30)
Delivery record fields
- Name
id- Type
- string
- Description
Unique delivery ID.
- Name
webhook_id- Type
- string
- Description
The webhook this delivery belongs to.
- Name
event_type- Type
- string
- Description
The event that triggered this delivery.
- Name
payload- Type
- object
- Description
The full JSON payload that was sent.
- Name
response_status- Type
- number
- Description
HTTP status code returned by the receiving server, or
0if the request failed entirely.
- Name
response_body- Type
- string
- Description
The response body returned by the receiving server.
- Name
attempt_count- Type
- number
- Description
Which attempt this delivery represents (1 for the initial attempt).
- Name
delivered_at- Type
- string
- Description
Timestamp of when this delivery was attempted.