Webhooks
Webhooks
Modula supports event-driven webhooks that send HTTP POST notifications to external URLs when CMS events occur. Webhooks are useful for triggering static site rebuilds, syncing content to external systems, sending notifications, or integrating with CI/CD pipelines.
Concepts
Webhook -- A registered endpoint that receives HTTP POST notifications when subscribed events occur. Each webhook has a URL, a list of events to subscribe to, an optional secret for signature verification, and optional custom headers.
Event -- A named occurrence in the CMS lifecycle (e.g., content.published, content.deleted). Webhooks subscribe to specific events and only receive payloads for those events.
Delivery -- A single attempt to send an event payload to a webhook URL. Each delivery records the HTTP status code, response, timing, and error information. Failed deliveries can be retried.
Secret -- An optional string used to sign webhook payloads with HMAC-SHA256. The signature is sent in the X-ModulaCMS-Signature header, allowing the receiver to verify that the payload came from ModulaCMS and was not tampered with.
Event Types
| Event | Fires when |
|---|---|
content.published |
Content is published (transitions from draft to published) |
content.unpublished |
Content is unpublished (reverts from published to draft) |
content.updated |
Published content fields are updated |
content.scheduled |
Content is scheduled for future publication |
content.deleted |
Content is deleted |
locale.published |
Locale-specific content is published |
version.created |
A new version snapshot is created |
admin.content.published |
Admin content is published |
admin.content.unpublished |
Admin content is unpublished |
admin.content.updated |
Admin content is updated |
admin.content.deleted |
Admin content is deleted |
webhook.test |
Synthetic test event (sent by the test endpoint) |
Use * as a wildcard to subscribe to all events.
Creating a Webhook
curl -X POST http://localhost:8080/api/v1/admin/webhooks \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"name": "Deploy trigger",
"url": "https://ci.example.com/hooks/modulacms",
"secret": "whsec_your_secret_key",
"events": ["content.published", "content.unpublished"],
"is_active": true,
"headers": {
"X-Custom-Header": "my-value"
}
}'
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable name for the webhook |
url |
string | Yes | HTTPS endpoint that receives POST requests |
secret |
string | No | Secret for HMAC-SHA256 payload signing. Auto-generated if omitted. |
events |
string[] | Yes | List of event types to subscribe to (or ["*"] for all) |
is_active |
bool | No | Whether the webhook is active. Defaults to false. |
headers |
object | No | Custom HTTP headers sent with each delivery |
The URL is validated against SSRF rules. By default, only HTTPS URLs are accepted. HTTP URLs can be allowed via the webhook_allow_http configuration option.
Managing Webhooks
Listing Webhooks
curl http://localhost:8080/api/v1/admin/webhooks \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Getting a Single Webhook
curl http://localhost:8080/api/v1/admin/webhooks/01JNRWDP6HMTY9S7Q1Z4B8K5FR \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Updating a Webhook
curl -X PUT http://localhost:8080/api/v1/admin/webhooks/01JNRWDP6HMTY9S7Q1Z4B8K5FR \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"name": "Deploy trigger (updated)",
"url": "https://ci.example.com/hooks/modulacms",
"events": ["content.published", "content.unpublished", "content.updated"],
"is_active": true
}'
Deleting a Webhook
curl -X DELETE http://localhost:8080/api/v1/admin/webhooks/01JNRWDP6HMTY9S7Q1Z4B8K5FR \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Webhook Delivery
When a subscribed event occurs, ModulaCMS POSTs a JSON payload to the webhook URL.
Payload Structure
{
"id": "01JNRWEP7INUZ0T8R2A5C9L6GS",
"event": "content.published",
"occurred_at": "2026-03-07T14:30:00Z",
"data": {
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"content_version_id": "01JNRWEP7HNTZ0T8R2A5C9L6GT",
"version_number": 3,
"locale": "",
"published_by": "01JNRWAM3ENRZ7R5N9X4C6K8DL"
}
}
Request Headers
Each delivery includes these headers:
| Header | Description |
|---|---|
Content-Type |
application/json |
X-ModulaCMS-Signature |
HMAC-SHA256 hex digest of the payload body, signed with the webhook secret |
X-ModulaCMS-Event |
The event type (e.g., content.published) |
User-Agent |
ModulaCMS-Webhook/1.0 |
Any custom headers configured on the webhook are also included.
Delivery Statuses
| Status | Description |
|---|---|
pending |
Queued for delivery |
success |
Delivered successfully (2xx response) |
failed |
Permanently failed after exhausting all retry attempts |
retrying |
Queued for retry after a previous failure |
Delivery History
View the delivery history for a webhook:
curl http://localhost:8080/api/v1/admin/webhooks/01JNRWDP6HMTY9S7Q1Z4B8K5FR/deliveries \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Each delivery record contains:
| Field | Description |
|---|---|
delivery_id |
ULID of this delivery attempt |
webhook_id |
ULID of the webhook |
event |
Event type that triggered this delivery |
payload |
The JSON payload that was sent |
status |
Delivery status (pending, success, failed, retrying) |
attempts |
Number of delivery attempts made |
last_status_code |
HTTP status code from the most recent attempt |
last_error |
Error message from the most recent failed attempt |
next_retry_at |
Scheduled time for the next retry (if retrying) |
created_at |
When the delivery was created |
completed_at |
When the delivery succeeded or was abandoned |
Retrying Failed Deliveries
Re-enqueue a failed delivery for another attempt:
curl -X POST http://localhost:8080/api/v1/admin/webhooks/deliveries/01JNRWEP7INUZ0T8R2A5C9L6GS/retry \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Only failed deliveries can be retried. Attempting to retry a successful delivery returns an error.
Testing Webhooks
Send a synthetic test event to verify the webhook endpoint is reachable and responds correctly:
curl -X POST http://localhost:8080/api/v1/admin/webhooks/01JNRWDP6HMTY9S7Q1Z4B8K5FR/test \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Response:
{
"status": "success",
"status_code": 200,
"duration": "142ms"
}
The test event uses the webhook.test event type. The payload includes the webhook ID and a test message. The test is synchronous -- it waits for the endpoint to respond and reports the result immediately.
Securing Webhook Endpoints
When a secret is configured on a webhook, every payload is signed with HMAC-SHA256. To verify the signature on your receiving end:
- Read the raw request body.
- Compute the HMAC-SHA256 of the body using the webhook secret as the key.
- Compare the hex-encoded result to the
X-ModulaCMS-Signatureheader value. - Reject the request if the signatures do not match.
Example verification in Go:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhook(r *http.Request, secret string) bool {
body, err := io.ReadAll(r.Body)
if err != nil {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
signature := r.Header.Get("X-ModulaCMS-Signature")
return hmac.Equal([]byte(expected), []byte(signature))
}
Example verification in Node.js:
const crypto = require('crypto')
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
)
}
SDK Examples
Go
import modula "github.com/hegner123/modulacms/sdks/go"
client, _ := modula.NewClient(modula.ClientConfig{
BaseURL: "http://localhost:8080",
APIKey: "mcms_YOUR_API_KEY",
})
// Create a webhook
webhook, err := client.Webhooks.Create(ctx, modula.CreateWebhookRequest{
Name: "Deploy trigger",
URL: "https://ci.example.com/hooks/modulacms",
Events: []string{"content.published", "content.unpublished"},
IsActive: true,
})
// List all webhooks
webhooks, err := client.Webhooks.List(ctx, nil)
// Test a webhook
testResp, err := client.Webhooks.Test(ctx, webhook.WebhookID)
// View delivery history
deliveries, err := client.Webhooks.ListDeliveries(ctx, webhook.WebhookID)
// Retry a failed delivery
err = client.Webhooks.RetryDelivery(ctx, deliveryID)
// Delete a webhook
err = client.Webhooks.Delete(ctx, webhook.WebhookID)
TypeScript
import { ModulaCMSAdmin } from '@modulacms/admin-sdk'
const client = new ModulaCMSAdmin({
baseUrl: 'http://localhost:8080',
apiKey: 'mcms_YOUR_API_KEY',
})
// Create a webhook
const webhook = await client.webhooks.create({
name: 'Deploy trigger',
url: 'https://ci.example.com/hooks/modulacms',
events: ['content.published', 'content.unpublished'],
is_active: true,
})
// List all webhooks
const webhooks = await client.webhooks.list()
// Test a webhook
const testResp = await client.webhooks.test(webhook.webhook_id)
// View delivery history
const deliveries = await client.webhooks.listDeliveries(webhook.webhook_id)
// Retry a failed delivery
await client.webhooks.retryDelivery(deliveryId)
API Reference
All webhook endpoints require authentication and webhooks:* permissions (admin-only by default).
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/webhooks |
List all webhooks |
| POST | /api/v1/admin/webhooks |
Create a webhook |
| GET | /api/v1/admin/webhooks/{id} |
Get a webhook |
| PUT | /api/v1/admin/webhooks/{id} |
Update a webhook |
| DELETE | /api/v1/admin/webhooks/{id} |
Delete a webhook |
| POST | /api/v1/admin/webhooks/{id}/test |
Send a test event |
| GET | /api/v1/admin/webhooks/{id}/deliveries |
List delivery history |
| POST | /api/v1/admin/webhooks/deliveries/{id}/retry |
Retry a failed delivery |
Configuration
Webhook-related configuration in modula.config.json:
| Field | Type | Default | Description |
|---|---|---|---|
webhook_enabled |
bool | false |
Enable the webhook dispatcher. Required for any webhook delivery. |
webhook_allow_http |
bool | false |
Allow HTTP (non-HTTPS) webhook URLs |
webhook_timeout |
int | 10 |
Timeout in seconds for webhook delivery requests |
webhook_max_retries |
int | 3 |
Maximum delivery attempts before marking as permanently failed |
webhook_workers |
int | 4 |
Number of concurrent delivery worker goroutines |
webhook_delivery_retention_days |
int | 30 |
Days to retain delivery history records |
Notes
- Auto-generated secrets. If no secret is provided when creating a webhook, the server generates a 32-byte random hex string.
- SSRF protection. Webhook URLs are validated to prevent server-side request forgery. Private IP ranges, loopback addresses, and non-HTTPS URLs are blocked by default.
- Wildcard events. Use
["*"]in the events list to subscribe to all event types, including any added in future versions. - No redirects. Webhook delivery does not follow HTTP redirects. A redirect response is treated as a non-2xx status.
- Must be enabled. Webhooks require
"webhook_enabled": trueinmodula.config.json. Without it, the dispatcher is not created and all webhook events silently no-op. - Automatic retries. Failed deliveries are retried with exponential backoff: 1 minute, 5 minutes, then 30 minutes. After
webhook_max_retriesattempts (default 3), the delivery is marked as permanently failed. A background process checks for retryable deliveries every 60 seconds.