Webhooks
Recibe eventos firmados de Ricky, verifícalos y procesa cada uno una sola vez.
Un webhook es una URL de tu sistema a la que Ricky envía un POST cuando ocurre un evento en la organización. Ricky firma cada entrega y la reintenta si falla.
La clave necesita el permiso webhooks:manage.
Crea un webhook
curl https://getricky.ai/api/v1/webhooks \
-H "Authorization: Bearer $RICKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/webhooks/ricky",
"event_types": ["message.received", "conversation.closed"]
}'{
"data": {
"id": "e2c4a6b8-0d1f-4e3a-9b5c-7d9f1a3c5e7b",
"url": "https://erp.example.com/webhooks/ricky",
"event_types": ["message.received", "conversation.closed"],
"status": "active",
"created_at": "2026-10-14T15:00:00.000Z",
"disabled_at": null,
"signing_secret": "whsec_…"
}
}Guarda signing_secret en tu gestor de secretos. Ricky solo lo muestra en esta respuesta.
La URL debe cumplir estas condiciones. Si no, la respuesta es 400 invalid_input con param: "url".
- Usa HTTPS en el puerto 443.
- Usa un nombre de dominio, no una dirección IP.
- No lleva usuario ni contraseña.
- El dominio resuelve a direcciones públicas.
Ricky vuelve a comprobar la URL en cada entrega y no sigue redirecciones.
Eventos
| Evento | Ocurre cuando | data |
|---|---|---|
message.received | Un contacto envía un mensaje. | conversation_id, message_id |
message.sent | El proveedor acepta un mensaje de la organización. | conversation_id, message_id |
conversation.opened | Se crea o se reabre una conversación. | conversation_id |
conversation.closed | Se cierra una conversación. | conversation_id |
conversation.assignment_changed | Se asigna, se reasigna o se quita la asignación de una conversación. | conversation_id |
message.sent llega por cada mensaje saliente, no solo por los enviados con la API.
El evento solo trae IDs. Para el contenido, lee el recurso con la API: Get a conversation o List messages.
La entrega
POST /webhooks/ricky HTTP/1.1
Host: erp.example.com
Content-Type: application/json
User-Agent: Ricky-Webhooks/1.0
X-Ricky-Delivery-Id: 5b8e0f3a-2c51-4b8e-9d0e-6f1a2b3c4d5e
X-Ricky-Timestamp: 1791992702
X-Ricky-Signature: v1=c4a06466fda49cd8401f485eb542ba41573b96f321758a2376acbffdd44c8ded
{"id":"5b8e0f3a-2c51-4b8e-9d0e-6f1a2b3c4d5e","type":"message.received","occurred_at":"2026-10-14T15:42:10.000Z","organization_id":"0d9c4b7e-1f2a-4c3d-8e5f-6a7b8c9d0e1f","data":{"conversation_id":"3f6d2a10-8b7c-4e1d-9a2f-5c4b3e2d1a09","message_id":"a7e5c3b1-9d8f-4a6e-b2c4-1d3f5e7a9b0c"}}| Cabecera | Valor |
|---|---|
X-Ricky-Delivery-Id | ID de la entrega. Es igual al id del cuerpo. |
X-Ricky-Timestamp | Hora Unix, en segundos, de este intento. Cambia en cada reintento. |
X-Ricky-Signature | v1= seguido del HMAC-SHA256 en hexadecimal. |
| Campo del cuerpo | Descripción |
|---|---|
id | ID de la entrega. Úsalo para descartar duplicados. |
type | Tipo de evento. |
occurred_at | Cuándo ocurrió, en ISO 8601 UTC. |
organization_id | Organización del evento. |
data | conversation_id siempre; message_id en los eventos message.*. |
Verifica la firma
Verifica cada entrega antes de usarla:
- Lee el cuerpo crudo, en bytes, antes de convertirlo a JSON. Un cuerpo re-serializado no coincide.
- Rechaza la entrega si
X-Ricky-Timestampdifiere de tu reloj en más de 5 minutos. - Calcula el HMAC-SHA256 de
<X-Ricky-Timestamp>.<cuerpo crudo>. La clave es elsigning_secretcompleto, con el prefijowhsec_. - Compara el resultado en hexadecimal con lo que sigue a
v1=, en tiempo constante.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const SECRET = process.env.RICKY_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 5 * 60;
export function verifyRickySignature(rawBody, timestamp, signature, secret = SECRET) {
if (!timestamp || !signature?.startsWith('v1=')) return false;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!(age <= TOLERANCE_SECONDS)) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
const received = Buffer.from(signature.slice(3), 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}
const app = express();
// express.raw() keeps the body as bytes, exactly as Ricky signed it.
app.post('/webhooks/ricky', express.raw({ type: 'application/json' }), async (req, res) => {
const valid = verifyRickySignature(
req.body,
req.get('X-Ricky-Timestamp'),
req.get('X-Ricky-Signature'),
);
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
await saveEventOnce(event); // your code: store it with event.id as a unique key
res.sendStatus(204);
});Prueba tu verificación
Con estos valores exactos, el HMAC debe coincidir. La comprobación de los 5 minutos falla con esta hora, así que pruébala aparte o fija el reloj en tu prueba.
| Dato | Valor |
|---|---|
| Secreto | whsec_test_5YtV0kq9mUz2Rr8Lw3Nf1Hc6Pb4Xa7Ds0Ge2Jo9Ki8 |
| Timestamp | 1791992702 |
| Cuerpo | El cuerpo del ejemplo de La entrega, sin espacios ni salto de línea final. |
| Firma | v1=c4a06466fda49cd8401f485eb542ba41573b96f321758a2376acbffdd44c8ded |
Responde
- Responde con cualquier estado
2xxen menos de 10 segundos. Ricky corta la conexión a los 10 segundos y lo cuenta como fallo. - Guarda el evento y responde. Haz el trabajo pesado después, fuera de la solicitud.
- Una redirección, un estado distinto de
2xx, un error de red o una respuesta de más de 64 KiB cuentan como fallo.
Cadencia y reintentos
Ricky envía las entregas en pasadas cada 15 minutos. Un evento nuevo o un reintento esperan a la siguiente pasada. Los webhooks no son un canal en tiempo real: si necesitas un dato antes, consúltalo con la API.
Si un intento falla, Ricky espera antes del siguiente: al menos 30 segundos después del primer fallo, y el doble cada vez, hasta un máximo de 1 hora. El intento ocurre en la primera pasada después de esa espera. Tras 10 intentos fallidos, la entrega queda en failed.
Duplicados y orden
- Una misma entrega puede llegar más de una vez, por ejemplo si tu servidor respondió después de los 10 segundos. Descarta duplicados por
id. - Las entregas pueden llegar en otro orden que los eventos. Ordénalas por
occurred_aty lee el estado actual con la API antes de actuar.
Registro de entregas
GET /webhooks/{webhook_id}/deliveries devuelve las 100 entregas más recientes de un webhook.
{
"data": [
{
"id": "5b8e0f3a-2c51-4b8e-9d0e-6f1a2b3c4d5e",
"event_type": "message.received",
"status": "failed",
"attempts": 10,
"last_response_status": 503,
"last_error": "HTTP 503",
"created_at": "2026-10-14T15:42:10.000Z",
"next_attempt_at": "2026-10-15T00:12:41.000Z",
"delivered_at": null
}
],
"next_cursor": null
}status | Significado |
|---|---|
pending | Espera su próximo intento, en next_attempt_at o después. |
processing | Ricky la está enviando. |
delivered | Tu servidor respondió 2xx. |
failed | Agotó sus 10 intentos. |
Reintento manual
Después de corregir tu servidor, vuelve a poner en cola una entrega failed:
curl -X POST https://getricky.ai/api/v1/webhooks/e2c4a6b8-0d1f-4e3a-9b5c-7d9f1a3c5e7b/deliveries/5b8e0f3a-2c51-4b8e-9d0e-6f1a2b3c4d5e/retry \
-H "Authorization: Bearer $RICKY_API_KEY"La entrega vuelve a pending con 10 intentos nuevos y sale en la siguiente pasada. Solo se reintentan entregas failed de un webhook activo; cualquier otra recibe 404 not_found.
Desactiva un webhook
DELETE /webhooks/{webhook_id} detiene las entregas nuevas y las pendientes. Un webhook desactivado no se puede reactivar: crea otro. Desactivarlo dos veces responde 404 not_found.
Para cambiar el secreto, crea un webhook nuevo, despliega su secreto y desactiva el anterior. Mientras ambos estén activos, cada evento llega dos veces, una por webhook, con id distintos.