Webhook trigger
Raytha Functions have no trigger called "webhook" in 2.0. You get the same result with two existing triggers: an HTTP request function receives webhooks from other services, and a content event function sends one when content changes. This page shows both, and explains what the 1.x "webhook trigger" was.
What changed from 1.x
In Raytha 1.x the trigger type labelled "Webhook" ran run(payload) when a content item was created, updated or deleted. In 2.0 those are three trigger types, content_item_created, content_item_updated and content_item_deleted, described in Content event triggers. Existing functions keep working. Raytha 2.0 also has a separate built-in Webhooks feature that delivers signed, retried HTTP calls to a URL when events happen. See Webhooks.
| You want to | Use |
|---|---|
| Receive a webhook from Stripe, GitHub, a form service or your own app | An HTTP request function, below. |
| Call another service when a content item changes, with custom logic or payload | A content event function that calls HttpClient, below. |
| Push content events to a URL with signing and retries and no code | The built-in Webhooks feature. |
Receive a webhook
An HTTP function is already a public endpoint, so it can receive a webhook at /raytha/functions/execute/inbound, or at a route path you choose. The example below does three things a receiver should always do: it checks a shared secret, it ignores events it has already processed, and it records each event before doing the work.
Create a content type webhook_events whose primary field is a single line text (the event id) and that has a long text field payload. Create an HTTP function named inbound:
var SHARED_SECRET = "change-me-to-a-long-random-string"; // give the sender .../inbound?token=<this>
var EVENTS_TEMPLATE_ID = "REPLACE_WITH_WEB_TEMPLATE_ID"; // any template that can render webhook_events
function param(list, name) {
var entry = list.find(function (i) { return i.Key === name; });
return entry && entry.Value.length > 0 ? entry.Value[0] : "";
}
function sameString(a, b) { // no early exit on the first difference
if (a.length !== b.length) { return false; }
var diff = 0;
for (var i = 0; i < a.length; i++) { diff |= a.charCodeAt(i) ^ b.charCodeAt(i); }
return diff === 0;
}
function post(payload, query) {
if (!sameString(param(query, "token"), SHARED_SECRET)) {
return new StatusCodeResult(401, "Unauthorized");
}
if (Array.isArray(payload) || !payload || !payload.id) {
return new StatusCodeResult(400, "Expected a JSON object with an id");
}
// Idempotency: a provider that retries must not cause the work twice.
var eventId = String(payload.id);
var seen = API_V1.GetContentItems("webhook_events", "", "", "PrimaryField eq '" + eventId.replace(/'/g, "''") + "'", "", 1, 1);
if (seen.Success && seen.Result.TotalCount > 0) {
return new JsonResult({ ok: true, duplicate: true });
}
var saved = API_V1.CreateContentItem("webhook_events", false, EVENTS_TEMPLATE_ID, {
title: eventId,
payload: JSON.stringify(payload)
});
if (!saved.Success) {
return new StatusCodeResult(500, "Could not record the event");
}
// ... do the real work here, after the event is recorded.
return new JsonResult({ ok: true });
}
Give the sender this URL, with your secret in place of the example:
curl -s -X POST "$RAYTHA_URL/raytha/functions/execute/inbound?token=change-me-to-a-long-random-string" \
-H "Content-Type: application/json" \
-d '{"id":"evt_1001","type":"order.paid","amount":4200}'
{
"ok": true
}
Sending the same event again returns {"ok": true, "duplicate": true} and does no work. The example needs a web template id for CreateContentItem: choose any template that has access to the content type. Find ids in the admin, or with GET /raytha/api/v1/WebTemplates.
Why the secret is in the URL
A function does not receive request headers or the raw body. That rules out the usual signature schemes, such as Stripe-Signature or X-Hub-Signature-256, because you can neither read the header nor recompute the HMAC over the original bytes. Functions also have no HMAC primitive: only Convert, Encoding and BitConverter are available for crypto-adjacent work. So use what a function can verify:
- A long random secret in the query string, compared in constant time as above. Configure it in the sender's webhook URL. Use HTTPS, because the query string is visible in server logs.
- A sender that lets you add a secret field to the JSON body. Compare it the same way.
- An allow-list on
CurrentUser.RemoteIpAddressfor senders with fixed addresses. Behind a proxy this address is only correct whenTRUSTED_PROXIESis set. See Running behind a proxy.
Idempotency
Most providers retry when you do not answer 2xx quickly, so the same event arrives more than once. Key the work on the provider's event id. The example stores the id as the primary field of a content item and looks it up first, using the filter PrimaryField eq '...' with single quotes doubled. The lookup and the insert are two steps, so two deliveries that arrive within milliseconds can both pass the check. If that matters, make the downstream work safe to repeat as well.
Answer fast
Return as soon as the event is stored. A function that exceeds RAYTHA_FUNCTIONS_TIMEOUT (10 seconds by default) is stopped and the sender sees 500. Do slow work in a content event function that checks payload.ContentType.DeveloperName === "webhook_events": creating the item fires content_item_created, which runs in the background.
Send a webhook when content changes
Create a function with the trigger Content item created and this code. Create more functions for updated and deleted if you need them.
// Trigger: Content item created. Runs for every content type, so filter on the type first.
function run(payload) {
if (payload.ContentType.DeveloperName !== "posts") {
return;
}
HttpClient.Post(
"https://hooks.example.com/notify",
{ "Authorization": "Bearer YOUR-TOKEN" },
{ event: "created", id: payload.Id, title: payload.PublishedContent.title }
);
}
A content event function runs for every content type, so the first line filters on payload.ContentType.DeveloperName. The payload is the content item with PascalCase keys, and the call runs in the background after the save. With json left at its default, HttpClient.Post sends the body as JSON (see Built-in objects). A response other than 2xx throws an exception, which is recorded on the background task. Nothing retries it. Use the built-in Webhooks feature if you need retries.
Gotchas
- No headers. Header signatures, bearer tokens sent by the caller, and
User-Agentchecks cannot be done in a function. - The body must be JSON or a form. A sender that posts
text/plainor XML gets a500before your code runs. - Private addresses are blocked for outbound calls.
HttpClientrefuses localhost and internal addresses unlessALLOW_INTERNAL_URL_IMPORTS=true. - Loops. A function on
content_item_updatedthat edits an item throughAPI_V1fires the same event again. Raytha has no recursion guard, so make the function check what changed before it writes. - Secrets in code. Anyone with Manage System Settings can read the function code and its secret.
Next steps
- Content event triggers: the payload shape and background execution.
- Webhooks: the built-in signed, retried delivery feature.
- HTTP request trigger: request and response reference.
- Recipes: contact form, feeds and redirects.