Ricky logorickyDocs

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"]
  }'
201 Created
{
  "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

EventoOcurre cuandodata
message.receivedUn contacto envía un mensaje.conversation_id, message_id
message.sentEl proveedor acepta un mensaje de la organización.conversation_id, message_id
conversation.openedSe crea o se reabre una conversación.conversation_id
conversation.closedSe cierra una conversación.conversation_id
conversation.assignment_changedSe 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"}}
CabeceraValor
X-Ricky-Delivery-IdID de la entrega. Es igual al id del cuerpo.
X-Ricky-TimestampHora Unix, en segundos, de este intento. Cambia en cada reintento.
X-Ricky-Signaturev1= seguido del HMAC-SHA256 en hexadecimal.
Campo del cuerpoDescripción
idID de la entrega. Úsalo para descartar duplicados.
typeTipo de evento.
occurred_atCuándo ocurrió, en ISO 8601 UTC.
organization_idOrganización del evento.
dataconversation_id siempre; message_id en los eventos message.*.

Verifica la firma

Verifica cada entrega antes de usarla:

  1. Lee el cuerpo crudo, en bytes, antes de convertirlo a JSON. Un cuerpo re-serializado no coincide.
  2. Rechaza la entrega si X-Ricky-Timestamp difiere de tu reloj en más de 5 minutos.
  3. Calcula el HMAC-SHA256 de <X-Ricky-Timestamp>.<cuerpo crudo>. La clave es el signing_secret completo, con el prefijo whsec_.
  4. 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.

DatoValor
Secretowhsec_test_5YtV0kq9mUz2Rr8Lw3Nf1Hc6Pb4Xa7Ds0Ge2Jo9Ki8
Timestamp1791992702
CuerpoEl cuerpo del ejemplo de La entrega, sin espacios ni salto de línea final.
Firmav1=c4a06466fda49cd8401f485eb542ba41573b96f321758a2376acbffdd44c8ded

Responde

  • Responde con cualquier estado 2xx en 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_at y 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.

200 OK
{
  "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
}
statusSignificado
pendingEspera su próximo intento, en next_attempt_at o después.
processingRicky la está enviando.
deliveredTu servidor respondió 2xx.
failedAgotó 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.

En esta página