ORYKSAORYKSADevelopers
Get API keys

Webhooks

Get a signed HTTPS request the moment something happens in the account.

Events

Event
message.repliedThe AI replied to a customer on WhatsApp, the website chat or the API. Includes the customer's message and the reply.
booking.createdThe AI booked an appointment or reservation (date, time, duration, type).
limit.reachedThe account used all the interactions of the plan for the current period.
knowledge.updatedThe Brain was updated through the API (pages, crawl or FAQ).

data.channel is whatsapp, web (website chat) or api.

Create a webhook

POST/v1/webhooks

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.