Webhooks
Recibe una petición HTTPS firmada en el momento en que algo pasa en la cuenta.
Eventos
| Evento | |
|---|---|
message.replied | La IA respondió a un cliente en WhatsApp, en el chat del sitio o por la API. Incluye el mensaje del cliente y la respuesta. |
booking.created | La IA creó una cita o reserva (fecha, hora, duración, tipo). |
limit.reached | La cuenta usó todas las interacciones del plan en el periodo actual. |
knowledge.updated | El Brain se actualizó por la API (páginas, lectura del sitio o FAQ). |
data.channel es whatsapp, web (chat del sitio) o api.
Crear un webhook
Créalo en la página Claves de API o por la API. El secret solo se devuelve una vez: guárdalo para verificar las firmas. Hasta 10 webhooks por cuenta; events puede ser una lista o ["*"] para todos.
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
}Contenido de los eventos
Todos los eventos tienen el mismo sobre: id, type, created_at y 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 }
}Verificar la firma
Cada petición lleva ORYKSA-Signature: t=<unix time>,v1=<hex>, donde v1 es el HMAC-SHA256 de t + "." + cuerpo en bruto con tu secreto. Comprueba también que t es reciente (por ejemplo, menos de 5 minutos) para bloquear repeticiones. Otras cabeceras: ORYKSA-Event y ORYKSA-Delivery (id del evento, útil para ignorar duplicados).
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);Entrega y reintentos
Responde con cualquier estado 2xx en 10 segundos. Si no, ORYKSA lo intenta 2 veces más (tras 2 y 4 segundos). Las redirecciones no se siguen. El último estado HTTP y el número de fallos seguidos aparecen en la página Claves de API y en GET /v1/webhooks. Usa Probar para enviar un evento webhook.test.

