ORYKSAORYKSADevelopers
Obter chaves de API

Webhooks

Recebe um pedido HTTPS assinado no momento em que algo acontece na conta.

Eventos

Evento
message.repliedA IA respondeu a um cliente no WhatsApp, no chat do site ou pela API. Inclui a mensagem do cliente e a resposta.
booking.createdA IA marcou uma marcação ou reserva (data, hora, duração, tipo).
limit.reachedA conta usou todas as interações do plano no período atual.
knowledge.updatedO Brain foi atualizado pela API (páginas, leitura do site ou FAQ).

data.channel é whatsapp, web (chat do site) ou api.

Criar um webhook

POST/v1/webhooks

Cria-o na página Chaves de API ou pela API. O secret só é devolvido uma vez: guarda-o para verificar as assinaturas. Até 10 webhooks por conta; events pode ser uma lista ou ["*"] 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
}

Conteúdo dos eventos

Todos os eventos têm o mesmo envelope: id, type, created_at e 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 a assinatura

Cada pedido leva ORYKSA-Signature: t=<unix time>,v1=<hex>, em que v1 é o HMAC-SHA256 de t + "." + corpo em bruto com o teu segredo. Confirma também que t é recente (por exemplo, menos de 5 minutos) para bloquear repetições. Outros cabeçalhos: ORYKSA-Event e ORYKSA-Delivery (id do 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 e novas tentativas

Responde com qualquer estado 2xx em 10 segundos. Caso contrário a ORYKSA tenta mais 2 vezes (após 2 e 4 segundos). Os redirecionamentos não são seguidos. O último estado HTTP e o número de falhas seguidas aparecem na página Chaves de API e em GET /v1/webhooks. Usa Testar para enviar um evento webhook.test.