Webhooks
Get a signed HTTPS request the moment something happens in the account.
Events
| Event | |
|---|---|
message.replied | The AI replied to a customer on WhatsApp, the website chat or the API. Includes the customer's message and the reply. |
booking.created | The AI booked an appointment or reservation (date, time, duration, type). |
limit.reached | The account used all the interactions of the plan for the current period. |
knowledge.updated | The Brain was updated through the API (pages, crawl or FAQ). |
data.channel is whatsapp, web (website chat) or api.
Create a webhook
Create it on the API keys page or through the API. The secret is returned only once: keep it to verify signatures. Up to 10 webhooks per account; events can be a list or ["*"] for all.
curl https://api.oryksa.com/v1/webhooks \
-H "Authorization: Bearer oryk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/oryksa-webhook", "events": ["message.replied", "booking.created"]}'{
"object": "webhook",
"id": "wh_3f2a9c0d1e7b4a5c8d9e",
"url": "https://example.com/oryksa-webhook",
"events": ["booking.created", "message.replied"],
"active": true,
"secret": "whsec_2b7c... (shown only once)",
"failures": 0
}Payloads
Every event has the same envelope: id, type, created_at and data.
{
"id": "evt_9b1f0d6c2a4e4f7e8a0b1c2d3e4f5a6b",
"type": "message.replied",
"created_at": "2026-09-24T09:41:07.512Z",
"data": {
"channel": "whatsapp",
"contact_id": "[email protected]",
"customer_message": "Do you have parking?",
"reply": "Yes, free parking right next to the winery!"
}
}{
"id": "evt_1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f",
"type": "booking.created",
"created_at": "2026-09-24T09:42:10.004Z",
"data": {
"channel": "web",
"contact_id": "web:1b2a9287-c821-437d-a8b5-7a1bd623bfe5",
"customer_name": "Ana",
"date": "2026-09-27",
"time": "10:00",
"duration": "60 min",
"type": "Winery visit"
}
}{
"id": "evt_...",
"type": "limit.reached",
"created_at": "2026-09-24T10:00:00Z",
"data": { "plan": "free" }
}{
"id": "evt_...",
"type": "knowledge.updated",
"created_at": "2026-09-24T10:05:00Z",
"data": { "source": "pages", "pages": 12, "chars": 48211 }
}Verify the signature
Each request carries ORYKSA-Signature: t=<unix time>,v1=<hex>, where v1 is the HMAC-SHA256 of t + "." + raw body with your secret. Also check that t is recent (for example less than 5 minutes) to block replays. Other headers: ORYKSA-Event and ORYKSA-Delivery (event id, useful to ignore duplicates).
import crypto from "node:crypto";
import express from "express";
const app = express();
// Keep the RAW body: the signature is computed over the exact bytes.
app.post("/oryksa-webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("ORYKSA-Signature") || ""; // t=1727170000,v1=abc...
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto
.createHmac("sha256", process.env.ORYKSA_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""))) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body);
if (event.type === "booking.created") {
// save the booking in your system
}
res.sendStatus(200);
});import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ORYKSA_WEBHOOK_SECRET"].encode()
@app.post("/oryksa-webhook")
def oryksa_webhook():
raw = request.get_data() # exact bytes
parts = dict(p.split("=", 1) for p in request.headers.get("ORYKSA-Signature", "").split(","))
expected = hmac.new(SECRET, f"{parts.get('t')}.".encode() + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
if event["type"] == "message.replied":
pass # e.g. copy the conversation to your CRM
return "", 200<?php
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_ORYKSA_SIGNATURE'] ?? ''), $p);
$expected = hash_hmac('sha256', $p['t'] . '.' . $raw, getenv('ORYKSA_WEBHOOK_SECRET'));
if (abs(time() - (int)$p['t']) > 300 || !hash_equals($expected, $p['v1'] ?? '')) {
http_response_code(400); exit;
}
$event = json_decode($raw, true);
http_response_code(200);Delivery and retries
Answer with any 2xx status within 10 seconds. Otherwise ORYKSA retries 2 more times (after 2 and 4 seconds). Redirects are not followed. The last HTTP status and the number of consecutive failures are visible on the API keys page and in GET /v1/webhooks. Use Test to send a webhook.test event.

