Skip to content

Webhooks

Oleg Ivanchenko edited this page Oct 18, 2026 · 4 revisions

Webhooks

oCMS webhooks allow you to receive real-time notifications when events occur in your CMS, enabling integration with external services and automation workflows.

Setting Up Webhooks

  1. Navigate to Admin > Webhooks (administrators only)
  2. Click New Webhook
  3. Configure:
    • Name: Descriptive name
    • URL: Your endpoint URL (HTTPS only when OCMS_REQUIRE_HTTPS_OUTBOUND is 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, Connection and other hop-by-hop headers are not allowed)
    • Active: Enable/disable
  4. Click Save

Testing

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.

Available Events

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.

Payload Format

{
    "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"
    }
}

Page Events

{
    "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.

Media Events

{
    "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
    }
}

Form Submission Events

{
    "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.

Security

Signature Verification

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

Request Headers

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.

Destination Rules

  • When OCMS_REQUIRE_HTTPS_OUTBOUND is on (the default in production), only https URLs are accepted; otherwise http is accepted too.
  • localhost and 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.

Destination Host Allowlisting

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

Delivery and Retry

Retry Schedule

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.

Retry Behavior

  • 5xx errors: Retried with exponential backoff
  • Timeout / 429: Retried with exponential backoff
  • 4xx errors (except 408, 429): NOT retried

Dead Letter Queue

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").

Event Debouncing

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

Monitoring

Delivery History

  1. Go to Admin > Webhooks
  2. Click the View Deliveries icon on the webhook row
  3. Browse the Delivery History page

Each delivery shows status (pending, delivered, or dead, labelled "Failed"), response code, attempt count, and timestamps.

Webhook Health

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).

Best Practices

  1. Always verify signatures — Protect against spoofed requests
  2. Respond quickly — Return 2xx immediately, process asynchronously
  3. Handle duplicates — Webhooks may occasionally be sent twice
  4. Use idempotent operations — Design handlers to be safely re-run
  5. Monitor failures — Set up alerts for repeated failures
  6. Keep secrets secure — Never expose webhook secrets

Example: Go Webhook Handler

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

Related

Clone this wiki locally