Repository navigation
Webhooks
oCMS webhooks allow you to receive real-time notifications when events occur in your CMS, enabling integration with external services and automation workflows.
- Navigate to Admin > Webhooks (administrators only)
- Click New Webhook
- Configure:
- Name: Descriptive name
-
URL: Your endpoint URL (HTTPS only when
OCMS_REQUIRE_HTTPS_OUTBOUNDis on, the default in production) - Secret: Auto-generated or custom secret for signature verification
- Events: Select which events to subscribe to
-
Headers: Optional custom HTTP headers (at most 20; names up to 64 characters, values up to 1024;
Host,Content-Length,Connectionand other hop-by-hop headers are not allowed) - Active: Enable/disable
- Click Save
Click Test on a webhook row to queue a test event; the retry worker picks it up, so it can take up to 30 seconds to arrive. For local testing, use webhook.site or ngrok.
| Event | Trigger |
|---|---|
page.created |
New page created |
page.updated |
Page modified |
page.deleted |
Page deleted |
page.published |
Page published |
page.unpublished |
Page unpublished |
media.uploaded |
Media uploaded |
media.deleted |
Media deleted |
form.submitted |
Form submitted |
user.created |
New user created |
user.deleted |
User deleted |
Events are dispatched by the admin UI handlers (pages, media, users and public form submissions), by the REST API for page and media writes (page.created, page.updated, page.deleted, media.uploaded, media.deleted), and by scheduled publishing (page.published). Creating or editing a page, a status change in the edit form or the REST API included, sends only page.created or page.updated; page.published comes from the publish toggle and scheduled publishing, page.unpublished only from the publish toggle. When the toggle and scheduled publishing publish a page at the same moment, only the first sends page.published. media.deleted is sent once the media's records are deleted, even when its files could not be removed from disk; the server logs the leftover files as an error, which the Event Log also records. Restoring a page version, creating a translation and content imports send no events. author_email is the email of the user who made the change, which can differ from author_id's user; events from the REST API and the scheduler carry none.
{
"type": "page.published",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": 123,
"title": "My Page Title",
"slug": "my-page-title",
"status": "published",
"author_id": 1,
"author_email": "admin@example.com"
}
}{
"type": "page.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": 123,
"title": "New Page",
"slug": "new-page",
"status": "draft",
"author_id": 1,
"author_email": "admin@example.com",
"language_code": "en"
}
}language_code is omitted when the page has none, and published_at is included once the page has been published.
{
"type": "media.uploaded",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": 456,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size": 1048576,
"uploader_id": 1
}
}{
"type": "form.submitted",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"form_id": 1,
"form_name": "Contact Form",
"form_slug": "contact",
"submission_id": 789,
"submitted_at": "2024-01-15T10:30:00Z"
}
}Note: Form submission data is controlled by OCMS_WEBHOOK_FORM_DATA_MODE (redacted, none, or full). See Configuration.
Every request includes an HMAC-SHA256 signature in X-Webhook-Signature. Verify it to ensure authenticity:
func verifySignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}| Header | Description |
|---|---|
Content-Type |
application/json |
User-Agent |
oCMS/1.0 |
X-Webhook-Signature |
HMAC-SHA256 signature |
X-Webhook-Event |
Event type (e.g., page.published) |
X-Webhook-Delivery-ID |
Unique delivery identifier |
Plus any custom headers you configure.
- When
OCMS_REQUIRE_HTTPS_OUTBOUNDis on (the default in production), onlyhttpsURLs are accepted; otherwisehttpis accepted too. -
localhostand URLs that resolve to private, loopback, link-local or other reserved addresses are rejected when the webhook is saved, before every delivery, and on every redirect.
In production, configure OCMS_WEBHOOK_ALLOWED_HOSTS (exact hostnames) to restrict webhook targets; with OCMS_REQUIRE_WEBHOOK_ALLOWED_HOSTS (on by default in production) the server refuses to start when active webhooks exist and no allowlist is set:
OCMS_WEBHOOK_ALLOWED_HOSTS=hooks.example.com,events.example.com| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 4 minutes |
| 5 | 8 minutes |
After 5 failed attempts, deliveries are marked as "dead". Delivered and dead deliveries are deleted after 30 days. A retry worker checks for due deliveries every 30 seconds.
- 5xx errors: Retried with exponential backoff
- Timeout / 429: Retried with exponential backoff
- 4xx errors (except 408, 429): NOT retried
Dead deliveries can be viewed in the webhook deliveries page and manually retried (resets attempt count). A pending retry whose webhook has been disabled in the meantime is marked dead ("webhook disabled").
To prevent webhook floods from rapid updates:
- Events for the same type and entity ID within a 1-second window are coalesced
- Only the latest event data is sent
- Maximum 5-second wait before dispatch
- Go to Admin > Webhooks
- Click the View Deliveries icon on the webhook row
- Browse the Delivery History page
Each delivery shows status (pending, delivered, or dead, labelled "Failed"), response code, attempt count, and timestamps.
The webhook list shows last 24h deliveries, the overall success rate, and a health indicator (green at 95% or more, yellow from 80% up to 95%, red below 80%, unknown with no deliveries).
- Always verify signatures — Protect against spoofed requests
- Respond quickly — Return 2xx immediately, process asynchronously
- Handle duplicates — Webhooks may occasionally be sent twice
- Use idempotent operations — Design handlers to be safely re-run
- Monitor failures — Set up alerts for repeated failures
- Keep secrets secure — Never expose webhook secrets
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
// Verify signature
signature := r.Header.Get("X-Webhook-Signature")
if !verifySignature(body, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Parse and handle
var payload struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
json.Unmarshal(body, &payload)
switch payload.Type {
case "page.published":
go triggerDeployment()
}
w.WriteHeader(http.StatusOK)
}- Configuration — Webhook environment variables
- Security — Webhook security overview
Getting Started
Core Features
- Content Management
- Media Library
- Taxonomy
- Menu Builder
- Forms
- Multi-Language
- SEO
- Feeds
- Video Embedding
- Agent-Ready
Administration
API & Integrations
Security
Theme Development
Module Development
Built-in Modules
- Module: Sentinel
- Module: Developer
- Module: DB Manager
- Module: Migrator
- Module: Informer
- Module: Demo Mode
- Module: Internal Analytics
- Module: External Analytics
- Module: Privacy
- Module: Embed
- Module: MCP Server
Deployment