ORYKSAORYKSADevelopers
Obtener claves de API

Webhooks

Recibe una petición HTTPS firmada en el momento en que algo pasa en la cuenta.

Eventos

Evento
message.repliedLa 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.createdLa IA creó una cita o reserva (fecha, hora, duración, tipo).
limit.reachedLa cuenta usó todas las interacciones del plan en el periodo actual.
knowledge.updatedEl 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

POST/v1/webhooks

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.