Webhooks
Reciba un aviso cuando ocurra algo: se captura un lead, un visitante pide hablar con una persona, una conversación cambia de estado. OneType envía un POST firmado a su endpoint HTTPS.
Estado: Disponible (Premium). El esquema de firma que figura abajo es definitivo, y el código de verificación de esta página se ha probado con el vector de prueba oficial de Standard Webhooks.
Configure un endpoint
En Developer › Webhooks (propietario o administrador):
- Añada una URL HTTPS y elija los eventos o todos ellos (
*). Opcionalmente elija Agents: el endpoint recibe entonces solo los eventos de esos agentes y ningún evento de todo el workspace. Si no elige agentes, recibe los eventos de todos los agentes y los de todo el workspace. - Copie el signing secret (
whsec_…). Se muestra una sola vez. Puede rotarlo más adelante. - Haga clic en Send test para entregar un
pingy vea la respuesta en el registro de entregas.
Eventos
| Tipo | Cuándo |
|---|---|
conversation.created | Un visitante inicia una conversación |
conversation.message.created | Se completa un mensaje (visitante, agente o su equipo) |
conversation.handoff.requested | El visitante o el agente pide hablar con una persona |
conversation.status.changed | El estado cambió: open, escalated, resolved, unresolved o failed (véase Estados de las conversaciones) |
lead.captured | Un visitante deja datos de contacto |
usage.threshold.reached | Su workspace alcanza el 50 %, el 80 % o el 100 % de sus créditos mensuales (respuestas en planes con clave propia) |
csat.submitted | Un visitante valoró una conversación (una vez, definitivo). data.object es un csat_rating con conversation, agent, rating (de 1 a 5), handled_by (ai o human), comment (las propias palabras del visitante, o null) y created_at. Incluye el comentario, así que trátelo como dato personal |
booking.made | Un visitante reservó una reunión desde la tarjeta de reservas del chat (requiere Cal.com o Calendly, próximamente). data.object es un booking con event_type, start_at, status, contact, conversation y matched_by (token: reservado desde la tarjeta de esta conversación; email: solo coincidió un correo electrónico verificado, no es una prueba) |
Con el tiempo se añadirán más tipos de evento. Ignore los tipos que no gestione. También pueden añadirse campos al envoltorio y a los objetos con el tiempo: ignore los que no use.
Payload
{
"id": "evt_01J9…",
"type": "lead.captured",
"created_at": "2026-09-22T10:00:00Z",
"api_version": "v1",
"workspace": "wsp_…",
"agent": "agt_…",
"data": { "object": { "id": "con_…", "object": "contact", "email": "jane@example.com", "name": "Jane" } }
}data.object es el mismo objeto que devuelve la REST API para ese recurso. Los objetos de mensaje incluyen is_ai_generated.
agent es el id del agente al que pertenece el evento, con la misma forma agt_… que usa la REST API. Está presente en los eventos sobre las conversaciones y los visitantes de un agente: conversation.created, conversation.message.created, conversation.handoff.requested, conversation.status.changed, csat.submitted, lead.captured y contact.identified. También está en booking.made cuando la reserva procedía de la conversación o de una persona de un agente. Se omite cuando no interviene ningún agente: eventos de todo el workspace como usage.threshold.reached, un booking.made que no coincidió con ningún agente y cambios de contacto que no procedían de un visitante (por ejemplo, una edición de su equipo). Trátelo como opcional y lea el recurso en data.object. El campo es una adición: los endpoints existentes empiezan a recibirlo sin ningún cambio, así que un receptor que rechace campos desconocidos debe permitir el campo agent.
Verificación de firmas
OneType firma cada entrega con Standard Webhooks, un esquema abierto que también usan varios grandes proveedores de API. Cada solicitud lleva tres cabeceras:
webhook-id: evt_01J9… (the same on every retry of this event)
webhook-timestamp: 1790000000 (Unix seconds)
webhook-signature: v1,K5oZfz…= (can hold several, space-separated, during secret rotation)La firma es base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), donde key es su secreto sin el prefijo whsec_ , decodificado en base64.
Siempre:
- verifique sobre el cuerpo en bruto de la solicitud, antes de cualquier análisis de JSON o nueva serialización
- rechace las marcas de tiempo a más de 5 minutos de la hora actual, lo que evita las repeticiones
- compare en tiempo constante
- acepte cualquier firma
v1de la lista, para que las rotaciones nunca le fallen
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyOnetypeSignature(rawBody, headers, secret, toleranceSeconds = 300)
{
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures)
{
throw new Error("missing webhook headers");
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds)
{
throw new Error("timestamp outside tolerance");
}
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
for (const entry of signatures.split(" "))
{
const [version, value] = entry.split(",");
const candidate = Buffer.from(value ?? "", "base64");
if (version === "v1" && candidate.length === expected.length && timingSafeEqual(candidate, expected))
{
return JSON.parse(rawBody);
}
}
throw new Error("no matching signature");
}
// Express: keep the raw body for this route
app.post("/hooks/onetype", express.raw({ type: "application/json" }), (req, res) => {
let event;
try { event = verifyOnetypeSignature(req.body.toString("utf8"), req.headers, process.env.ONETYPE_WEBHOOK_SECRET); }
catch { return res.sendStatus(400); }
if (event.type === "lead.captured") crm.upsert(event.data.object);
res.sendStatus(204);
});Para enrutar por agente, lea event.agent (por ejemplo if (event.agent === "agt_…") …); no aparece en los eventos de todo el workspace. O dé a cada agente su propio endpoint con Agents (agentIds en la API): se rechaza una reentrega que el endpoint ya no acepta, y una prueba se firma como uno de sus agentes.
Python
import base64, hashlib, hmac, json, time
def verify_onetype_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300):
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not msg_id or not timestamp or not signatures:
raise ValueError("missing webhook headers")
if abs(time.time() - int(timestamp)) > tolerance_seconds:
raise ValueError("timestamp outside tolerance")
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for entry in signatures.split(" "):
version, _, value = entry.partition(",")
if version == "v1" and hmac.compare_digest(value, expected):
return json.loads(raw_body)
raise ValueError("no matching signature")
# Flask: request.get_data() is the raw body; request.headers is case-insensitivePHP
<?php
function verifyOnetypeSignature(string $rawBody, array $headers, string $secret, int $toleranceSeconds = 300): array
{
$id = $headers['webhook-id'] ?? null;
$timestamp = $headers['webhook-timestamp'] ?? null;
$signatures = $headers['webhook-signature'] ?? null;
if (!$id || !$timestamp || !$signatures) {
throw new RuntimeException('missing webhook headers');
}
if (abs(time() - (int) $timestamp) > $toleranceSeconds) {
throw new RuntimeException('timestamp outside tolerance');
}
$key = base64_decode(preg_replace('/^whsec_/', '', $secret), true);
$expected = base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true));
foreach (explode(' ', $signatures) as $entry) {
[$version, $value] = array_pad(explode(',', $entry, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $value)) {
return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}
}
throw new RuntimeException('no matching signature');
}
// Usage: $raw = file_get_contents('php://input');
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);Herramientas sin código (Zapier, Make)
Sus catch hooks básicos aceptan cualquier solicitud y no pueden comprobar la firma. Trate la URL del hook como un secreto. Cuando la herramienta ofrezca un paso de código, haga ahí la misma comprobación: necesita el cuerpo en bruto , las tres cabeceras webhook-* y su secreto. Deduplique por webhook-id en cualquier caso, porque un reintento puede entregar un evento dos veces. Hay una guía paso a paso para propietarios en el centro de ayuda: Conectar Zapier o Make.
Compruebe su implementación con el vector de prueba de Standard Webhooks:
- secret
whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw - id
msg_p5jXN8AQM9LWM0D4loKWxJek - timestamp
1614265330 - body
{"test": 2432232314} - resultado esperado
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Respuestas y reintentos
- Responda cualquier 2xx en 10 segundos. Haga el trabajo lento después de responder, p. ej. poniéndolo en una cola.
- Ante una respuesta que no sea 2xx o un timeout, reintentamos durante unos 3 días: a los 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h y después cada 24 h tres veces.
- La entrega es al menos una vez. Guarde
webhook-ide ignore las repeticiones. - No se garantiza el orden. Use
created_atcuando importe el orden. - Un endpoint que solo falla durante 3 días queda desactivado, y los propietarios ven un banner. Vuelva a activarlo en el dashboard; los nuevos eventos fluyen desde entonces.
- El registro de entregas (30 días) muestra cada intento con su estado y su duración. Redeliver vuelve a enviar un evento.
- Solo entregamos a direcciones HTTPS públicas y no seguimos redirecciones.