Platform
Learn
Developer docs User guide Quickstart Blog
Company
Services About Contact Links Get started

Webhooks

Updated

A webhook is a web address that Raytha calls whenever something happens: a post is created, a user is edited, a page is published. Use webhooks to rebuild a static site, post to a chat channel, or keep another database in sync, without anyone polling the API.

What you need

  • The Manage System Settings permission.
  • An address on the public internet that accepts a POST request with a JSON body. A free request-inspection site works for a first test.

Create a webhook

  1. Open Automation > Webhooks and click New webhook.
  2. Fill in the form:
    • Name (required), for example "Rebuild site".
    • URL (required). A complete http:// or https:// address. Use HTTPS for anything real.
    • Description, for your own notes.
    • Subscribed events: comma-separated event names, or * for everything. Example: content_item.created,content_item.updated.
    • Max attempts: 1 to 10, default 5. This counts the first try plus retries.
    • Timeout (seconds): 1 to 120, default 30. How long Raytha waits for your server to answer.
    • Active: ticked by default.
  3. Click Create.
  4. On the Webhook created screen, click Copy beside the Signing secret, store it, then click Continue to webhook.

You should see the new webhook in the list with a green Active badge. The secret is shown once. The webhook page never displays it again. If you lose it, delete the webhook and create a new one.

Choose events

The form takes event names as plain text, so type them exactly. Names are matched without regard to case. Any name Raytha does not know is rejected with a message such as "content_item.publish is not a known webhook event."

AreaEvents
Contentcontent_item.created, content_item.updated, content_item.settings_updated, content_item.unpublished, content_item.deleted, content_item.bulk_deleted, content_item.restored, content_item.templates_assigned
Content typescontent_type.created, content_type.updated, content_type.deleted
Site pagessite_page.created, site_page.updated, site_page.published, site_page.unpublished, site_page.deleted
Mediamedia_item.created, media_item.deleted
Usersuser.created, user.updated, user.deleted, user.active_changed
Administratorsadmin.created, admin.updated, admin.deleted
Rolesrole.created, role.updated, role.deleted

There is no "published" event for content items, only content_item.unpublished. A change that publishes an item arrives as content_item.created or content_item.updated, so subscribe to those and check the item's state through the API. Start with a short list. * is convenient while testing but sends a request for every change in the system.

What Raytha sends

Each delivery is an HTTP POST with a JSON body and these headers:

HeaderValue
X-Raytha-EventThe event name, for example content_item.created.
X-Raytha-DeliveryA unique id for this delivery. The same id is used on every retry.
X-Raytha-TimestampThe time of this attempt, in ISO 8601 form.
X-Raytha-Signaturesha256= followed by an HMAC of the timestamp and body, made with your signing secret.
{
  "id": "0f6c9d1e-6a0b-4a52-9d41-3b2d0a8e7c11",
  "event": "content_item.created",
  "timestamp": "2026-10-02T14:03:11Z",
  "data": {
    "result": "3m1kQWm0wUeV9n6d1j8o2A",
    "request": { "SaveAsDraft": true, "ContentTypeDeveloperName": "posts", "Content": { "Title": "Spring sale" } },
    "requestType": "ContentItems.Commands.CreateContentItem+Command"
  }
}

data.result is what the action returned, usually the id of the thing it changed. data.request is the data that was submitted. Fields whose names contain password, secret, token or API key are removed. If you need the full current item, call the headless API with the id. See Create an API key for the headless REST API.

Verify the signature

Anyone who learns your URL can send it fake requests. Check the signature before you trust a delivery: compute the HMAC-SHA256 of the X-Raytha-Timestamp value, a period, and the raw request body, using the signing secret as the key, and compare it with the header.

import crypto from "node:crypto";

function isFromRaytha(rawBody, timestamp, signature, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Use the body exactly as received, before any JSON parsing, or the signature will not match. Also reject a delivery whose timestamp is more than a few minutes old, so a captured request cannot be replayed. For other languages, see Webhooks in the developer docs.

Retries and failures

A delivery succeeds when your server answers with any 2xx status within the timeout. Anything else counts as a failure: another status code, a timeout, or no connection. After a failure Raytha waits and tries again, doubling the wait each time (2, 4, 8 seconds and so on, never more than 60 seconds), until it reaches Max attempts. Then the delivery is marked failed and is not retried again.

Because of retries, your server may receive the same event twice. Use the X-Raytha-Delivery id to ignore a repeat. Respond quickly and do slow work afterwards.

Check that deliveries are happening

Each delivery runs as a background task. Open Settings > Background tasks and look for rows named "Deliver webhook task". A finished row reads "Delivered to your URL (HTTP 200)." A failing one shows the attempt number and the reason, for example "Attempt 2 of 5 failed (Received HTTP 500.); retry in 4s." Delivery history is deleted after the retention period for background tasks; see Update configuration and system-wide settings.

The 2.0.0 admin has no Send test button for webhooks. To test, subscribe to content_item.created, create a draft content item, and watch Background tasks and your receiving server.

Pause, edit or delete

  • Pause: open the webhook, clear Active, click Save. The list shows Inactive. Events that happen while it is paused are not queued for later.
  • Edit: change any field and click Save. The signing secret stays the same.
  • Delete: use Delete webhook under the form. Its delivery history is removed and pending deliveries are not sent.

Gotchas

  • Internal addresses are blocked by default. Raytha refuses to call localhost and private network addresses, so a webhook pointing at another container or an office server fails. The person who runs the server can allow them with the ALLOW_INTERNAL_URL_IMPORTS setting. See Configuration.
  • The URL must answer from the server, not your browser. A page that works when you open it in a browser can still be unreachable from Raytha's server because of a firewall.
  • Typos in event names are rejected, but a valid name that never fires looks like silence. Check the event name against the table above if nothing arrives.
  • Not every route fires an event. Items created one by one in the admin or the API, and in a batch through the API, send content_item.created. A CSV import does not appear to. Test the exact action you care about.
  • Treat the secret like a password. If it leaks, delete the webhook and create a new one.

Next steps