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_deliveries table 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 })
})

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 changes object 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_state and to_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 id and action field, 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).

AttemptDelay
1Immediate
24 seconds
38 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.


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
}

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 0 if 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.

Was this page helpful?