Introducción
Ovianta envía una petición HTTPS POST firmada a la URL que configures cada vez que ocurre un evento en tu cuenta. Así tu sistema (un CRM, una cola de tareas, tu propio backend) se entera al momento sin tener que consultarnos.
Hay dos eventos, más uno de prueba:
Evento | Cuándo se envía | Para qué sirve |
| Una vez por conversación de un agente, por teléfono o desde el widget web, entrante o saliente, contestada o no. | Archivar la conversación y conciliar lo que el agente hizo en tu agenda. |
| Cada vez que se deja una nota para el equipo de la clínica, la escriba quien la escriba. | Avisar a recepción de que hay trabajo pendiente. |
| Solo cuando pulsas "Enviar evento de prueba" en Ajustes. | Comprobar que tu endpoint recibe y verifica bien. |
Los webhooks son una notificación de un solo sentido: lo que respondas no cambia nada en Ovianta, solo confirma que lo recibiste.
Configuración
Un administrador de tu organización da de alta los endpoints en Ajustes → Webhooks de Ovianta.
Pulsa Añadir endpoint.
Escribe la URL de tu servidor y, si quieres, una descripción para reconocerlo.
Marca los eventos que quieres recibir. Un endpoint nuevo viene con todos marcados.
Abre el endpoint y copia la clave de firma (
whsec_…) con Revelar. La necesitas para verificar cada petición.Pulsa Enviar evento de prueba para comprobar que tu servidor responde.
Requisitos de la URL:
Tiene que empezar por
https://.Tiene que apuntar a un servidor público. Se rechazan
localhost, las IP privadas y las direcciones internas, también si un dominio público resuelve a una de ellas.No puede llevar usuario ni contraseña.
No seguimos redirecciones: una respuesta 3xx cuenta como rechazo.
Puedes tener hasta 10 endpoints por organización, cada uno con su clave y sus eventos. Si añadimos un tipo de evento nuevo, no se activa solo en tus endpoints: tendrás que marcarlo.
En el panel de cada endpoint ves sus peticiones de los últimos 30 días, con su estado, el número de intentos y el último código HTTP que devolvió tu servidor.
La petición
Cada evento llega como un POST con cuerpo JSON en UTF-8.
POST https://tu-servidor.com/webhooks/ovianta
Content-Type: application/json; charset=utf-8
Accept: application/json
User-Agent: Ovianta-Webhooks/1.0
X-Signature: 4f3c9a1e8b7d2c05e6a1b9d84c73f210a5e8b6d4c92f10e73a8b45d6c1f20e93
X-Ovianta-Event: call.ended
X-Ovianta-Event-Id: evt_9f2c1a7b4e5d40c8a1b2c3d4
X-Ovianta-Delivery-Attempt: 1
Cabecera | Qué lleva |
| Firma HMAC-SHA256 del cuerpo, en hexadecimal en minúsculas. Ver Verificar la firma. |
| Tipo de evento. Sirve para enrutar antes de leer el JSON. |
| Id del evento, el mismo que |
| Número de intento, de 1 a 5. |
|
|
Las cabeceras no van firmadas: si alguna no coincide con el cuerpo, manda el cuerpo.
Todos los eventos comparten el mismo sobre. Lo propio de cada tipo va dentro de data:
{
"event": "call.ended",
"eventId": "evt_9f2c1a7b4e5d40c8a1b2c3d4",
"version": 1,
"sentAt": "2026-10-06T13:24:43.253+02:00",
"attempt": 1,
"data": { }
}Campo | Tipo | Descripción |
| texto | Tipo de evento. |
| texto | Id del evento. Es igual en todos los reintentos y en todos tus endpoints que lo reciben. |
| número | Versión del contrato de ese tipo de evento. |
| fecha y hora | Cuándo salió este intento. |
| número | Número de intento, de 1 a 5. |
| objeto | El contenido del evento. |
Todas las fechas van en ISO 8601 con desfase horario, en hora de España peninsular (por ejemplo 2026-10-06T13:24:43+02:00).
Verificar la firma
Rechaza cualquier petición cuya firma no coincida. El cuerpo contiene datos de pacientes y la firma es lo que prueba que viene de Ovianta y que nadie lo ha modificado.
X-Signature es el HMAC-SHA256 del cuerpo tal cual llega, con la clave de firma del endpoint (la cadena completa whsec_…, prefijo incluido) como clave, en hexadecimal en minúsculas. Para verificarlo:
Lee el cuerpo en bruto, antes de convertirlo a JSON. Si lo parseas y lo vuelves a serializar, los bytes cambian y la firma no cuadra.
Calcula el HMAC-SHA256 de esos bytes con tu clave.
Compáralo con
X-Signatureusando una comparación de tiempo constante.Opcional, contra reenvíos:
sentAtva dentro del cuerpo firmado, así que puedes rechazar los eventos con más de unos minutos de antigüedad.
Si crees que tu clave se ha filtrado, borra el endpoint y crea otro: tendrá una clave nueva.
Entrega y reintentos
Responde con un 2xx en menos de 10 segundos y procesa el evento después, en segundo plano. Un endpoint lento acaba en un reintento, y un reintento en un duplicado que tendrás que filtrar.
Tu respuesta | Qué hacemos |
2xx | Entregado. No se reintenta. El cuerpo de tu respuesta se ignora. |
408, 429, 5xx, sin respuesta en 10 s, error de conexión o de TLS | Fallo temporal: se reintenta según el calendario. |
Cualquier otro 4xx, o una redirección 3xx | Rechazo definitivo: no se reintenta. |
Calendario de reintentos:
Intento | Espera desde el anterior | Tiempo desde el evento (aprox.) |
1 | — | inmediato |
2 | 1 min | 1 min |
3 | 5 min | 6 min |
4 | 15 min | 21 min |
5 | 40 min | 1 h |
Si respondes con una cabecera Retry-After, la respetamos cuando pide esperar más que el calendario, hasta un máximo de 1 hora. Tras el quinto fallo dejamos de intentarlo y la entrega queda como fallida en el panel del endpoint.
Antes de cada intento volvemos a leer tu configuración: si has borrado o deshabilitado el endpoint, o ya no está suscrito a ese evento, la entrega se cancela.
Duplicados y orden:
La entrega es "al menos una vez". Descarta duplicados por
eventId(oX-Ovianta-Event-Id), que es el mismo en todos los reintentos.No garantizamos el orden. Dos eventos casi simultáneos pueden llegar en cualquier orden. Si te importa la secuencia, ordena por
data.call.endedAtodata.note.createdAt.Tamaño. Un
call.endedcorto ocupa unos 2 KB; una conversación larga, varias decenas de KB. No limites el cuerpo por debajo de 256 KB.
Evento call.ended
Se envía una vez por cada conversación que termina un agente: entrante o saliente, por teléfono o desde el widget web, contestada o no. WhatsApp no genera call.ended.
Llega cuando Ovianta ha terminado de procesar la llamada, normalmente entre unos segundos y un minuto después de colgar. Si la llamada se transfirió a una persona, esperamos a saber cómo acabó esa transferencia, hasta 10 minutos.
data
Campo | Tipo | Descripción |
| objeto o | El agente: |
| objeto o | El centro de la llamada: |
| objeto | La conversación. Campos abajo. |
data.call
Campo | Tipo | Descripción |
| texto | Id de la llamada en Ovianta. Cítalo en cualquier consulta a soporte. |
| enum |
|
| enum |
|
| enum |
|
| fecha y hora | Inicio de la conversación. |
| fecha y hora | Fin de la conversación con el agente. En una transferencia, cuando el agente pasó la llamada, no cuando acabó la conversación con la persona. |
| número | Duración en segundos. |
| enum | Si hubo conversación. Ver Valores posibles. |
| enum o | Quién o qué la terminó. |
| objeto |
|
| objeto o | El paciente, si lo hay. |
| objeto o | La transferencia a una persona. |
| lista | Lo que el agente cambió en tu sistema. Puede estar vacía. |
| lista | La conversación por turnos. Vacía si no se contestó. |
data.call.phone
Campo | Descripción |
| Número que llama: el paciente en una entrante, la línea del agente en una saliente. |
| Número al que se llama: la línea del agente en una entrante, el paciente en una saliente. |
| El teléfono real del paciente. Si la llamada llegó desde una centralita o un intermediario, es |
data.call.patient
Campo | Descripción |
| Id del paciente en Ovianta, o |
| Id del paciente en tu sistema, si el agente lo buscó o lo dio de alta durante la llamada. |
| Nombre del paciente en Ovianta, o |
data.call.transfer
Campo | Descripción |
| Siempre |
| Clave de la regla de transferencia que se aplicó, por ejemplo |
| Número al que se transfirió, en E.164. |
| Resumen que el agente le pasó a quien atendió, o |
| Cómo acabó la transferencia. Ver Valores posibles. |
| Si se llegó a conectar al paciente con el destino, o |
| Duración de la parte con la persona, sin contar el tono, o |
| Cuándo acabó la parte con la persona, o |
data.call.actions[] — solo operaciones que escriben (las búsquedas y consultas de disponibilidad no aparecen), en el orden de la conversación.
Campo | Descripción |
| La operación. Ver Valores posibles. |
|
|
| Id de la cita en tu sistema, si lo hay. |
| Id del paciente en tu sistema, si lo hay. |
| Qué se reservó o movió: |
| En un |
| Si falló: |
data.call.transcript[]
Campo | Descripción |
|
|
| Segundos desde el inicio de la llamada. |
| Lo que se dijo, transcrito. Puede no venir en un turno que solo usó herramientas. |
| Herramientas que el agente usó en ese turno, en orden: |
La traza de toolCalls sirve para diagnosticar y puede cambiar sin aviso. Para conciliar con tu agenda, usa actions.
Evento note.created
Se envía cada vez que se deja una nota para el equipo de la clínica, la cree quien la cree: el agente durante una llamada o un chat de WhatsApp, el análisis posterior de la conversación o una regla automática (por ejemplo, una transferencia que nadie atendió). Llega unos segundos después de crearse la nota.
Es independiente de call.ended: la nota trae lo necesario para actuar (a quién llamar, por qué y los campos que tu clínica pide en cada nota) sin cruzarla con nada más. Si además quieres archivarla con su llamada, note.callId es el data.call.id de su call.ended, y la misma nota, con el mismo note.id, llega también dentro de la acción create_note de ese evento.
data
Campo | Tipo | Descripción |
| texto | Id de la nota en Ovianta. |
| fecha y hora | Cuándo se creó. |
| texto o | Id de la llamada en Ovianta de la que salió la nota, el mismo que |
| texto o | El motivo: lo que hay que hacer. |
| texto o | Nombre del paciente, tal como se capturó. |
| texto o | Teléfono de contacto, en E.164. |
| lista | Los campos que el agente rellena en cada nota (los que tu clínica configuró: aseguradora, ciudad, tipo de paciente…). Ver |
| enum |
|
| objeto o | El agente: |
| objeto o | El centro del agente: |
data.note.fields[] — siempre todos los campos del agente, en el orden en que están configurados, también los que esta nota dejó vacíos: así cada nota trae la misma lista.
Campo | Descripción |
| Id estable del campo. Úsalo para mapearlo en tu sistema. |
| Nombre del campo tal como lo ve la clínica. Puede cambiar si lo renombran: no mapees por él. |
| Lo que se capturó: texto, número o booleano. |
En este evento solo se notifica la creación: los cambios de estado posteriores de la nota no generan eventos.
Evento webhook.test
Se envía solo al pulsar Enviar evento de prueba en el panel del endpoint. Va firmado con la clave real, lleva la cabecera X-Ovianta-Test: true, se envía una sola vez sin reintentos, y el código que devuelva tu servidor aparece en pantalla.
"data": {
"message": "Test event from Ovianta. Respond with a 2xx status to confirm the endpoint works."
}Responde con un 2xx aunque tu servidor no procese este tipo de evento: así la prueba confirma que la URL y la verificación de firma funcionan.
Valores posibles
Pueden aparecer valores nuevos sin cambio de versión. Si recibes uno que no conoces, trátalo como el caso neutro más cercano en lugar de como un error.
call.outcome
Valor | Significado |
| Hubo conversación y llegó a su fin. |
| Llamada saliente no contestada o comunicando. La transcripción va vacía. |
| Contestó un buzón de voz. |
| La llamada no se pudo establecer o se cortó por un error técnico. |
call.endedBy
Valor | Significado |
| Colgó el paciente o cerró el widget. |
| El agente cerró la conversación. |
| El agente pasó la llamada a una persona. |
| Se cortó por silencio prolongado o por superar la duración máxima. |
| Se cortó por un fallo técnico. |
| No aplica: la llamada no llegó a conectar. |
transfer.outcome
Valor | Significado |
| El destino descolgó y se conectó con el paciente. Puede haber sido un buzón de voz del destino. |
| El destino comunicaba. |
| El destino no contestó. |
| La transferencia no se completó, incluido el rechazo por parte del destino. |
| Se hizo la transferencia pero no supimos cómo acabó. No es un fallo. |
actions[].type
Valor | Significado |
| Se reservó una cita nueva. |
| Se movió una cita existente. |
| Se canceló una cita. |
| El paciente confirmó su asistencia. |
| Se cambiaron datos de una cita existente (tipo, estado, duración…). |
| Se dio de alta un paciente en tu sistema. |
| Se dejó una nota para la clínica. La nota completa va en |
| Se pasó la llamada a una persona. Los detalles van en |
error.type
Valor | Significado |
| Tu sistema lo rechazó por una regla de negocio: hueco ocupado, cita ya cancelada, paciente no admitido. Esperado. |
| Faltaba un dato obligatorio o la petición no era válida. |
| Tu sistema devolvió un error o no respondió. Conviene revisarlo. |
| Fallo sin clasificar. |
Ejemplos
Datos ficticios. La transcripción del primero está recortada para que se lea mejor; en un evento real van todos los turnos.
call.ended: llamada entrante, cita movida, una cancelación rechazada y transferencia a recepción
{
"event": "call.ended",
"eventId": "evt_9f2c1a7b4e5d40c8a1b2c3d4",
"version": 1,
"sentAt": "2026-09-01T11:41:12.482+02:00",
"attempt": 1,
"data": {
"agent": { "id": "7c1f04d8-3b62-4a19-8f30-9d1e2c7a5b44", "name": "Recepción Sant Jordi", "type": "appointments_booking" },
"center": { "id": "b81e5a2c-7f14-4d80-9c66-3ab0e1d95f27", "name": "Sant Jordi Eixample", "externalId": "CEN-02" },
"call": {
"id": "3ab6f0d2-59c4-4f0e-9a71-2b8d5c6e1f10",
"direction": "inbound",
"channel": "phone",
"modality": "voice",
"startedAt": "2026-09-01T11:36:05.000+02:00",
"endedAt": "2026-09-01T11:39:41.000+02:00",
"durationSecs": 216,
"outcome": "completed",
"endedBy": "transfer",
"phone": { "from": "+34600112233", "to": "+34932556677", "patient": "+34600112233" },
"patient": { "id": "5d21b7e0-8c43-4f52-b1a9-6e07d4c81b33", "externalId": "P-99120", "name": "Marta Ruiz" },
"transfer": {
"requestedBy": "agent",
"reason": "consultas_seguros",
"destination": "+34932556680",
"summary": "Marta Ruiz pregunta si su seguro cubre la prueba de esfuerzo.",
"outcome": "answered",
"connected": true,
"durationSecs": 84,
"endedAt": "2026-09-01T11:41:05.000+02:00"
},
"actions": [
{
"type": "reschedule_appointment",
"status": "executed",
"appointmentId": "A-448122",
"patientId": "P-99120",
"appointment": { "startsAt": "2026-09-08T10:30:00+02:00", "doctorId": "DR-14", "serviceId": "SRV-3", "centerId": "CEN-02" }
},
{
"type": "cancel_appointment",
"status": "failed",
"appointmentId": "A-441097",
"error": { "type": "business_rule", "code": "APPOINTMENT_PREVIOUSLY_CANCELLED", "message": "The clinic's system rejected the operation." }
},
{
"type": "create_note",
"status": "executed",
"patientId": "P-99120",
"note": {
"id": "f4d83dde-f150-4891-b8cb-d42928e9cfbe",
"createdAt": "2026-09-01T11:38:57.785+02:00",
"callId": "3ab6f0d2-59c4-4f0e-9a71-2b8d5c6e1f10",
"patientName": "Marta Ruiz",
"phone": "+34600112233",
"callReason": "Pregunta si el seguro cubre la prueba de esfuerzo. Prefiere que la llamen por la tarde.",
"fields": [
{ "key": "customField1", "label": "Aseguradora", "value": "Sanitas" },
{ "key": "customField2", "label": "Franja preferida", "value": "Tarde" },
{ "key": "customField3", "label": "Nº de póliza", "value": null }
]
}
},
{ "type": "transfer_to_human", "status": "executed" }
],
"transcript": [
{ "role": "agent", "at": 0, "text": "Clínica Sant Jordi, le atiende el asistente virtual. ¿En qué puedo ayudarle?" },
{ "role": "patient", "at": 5, "text": "Quería cambiar la cita del jueves con el doctor Vidal." },
{
"role": "agent",
"at": 52,
"toolCalls": [
{
"name": "reschedule_appointment",
"status": "success"
}
]
},
{ "role": "agent", "at": 58, "text": "Su cita queda cambiada al martes 8 a las 10:30." }
]
}
}
}call.ended: llamada saliente que nadie contestó
{
"event": "call.ended",
"eventId": "evt_c40b81de73f24a90b1c2d3e4",
"version": 1,
"sentAt": "2026-09-01T17:22:09.771+02:00",
"attempt": 1,
"data": {
"agent": { "id": "e2905ba7-1cd4-49f8-b0a2-7f4c33e1d8a6", "name": "Recordatorios Sant Jordi", "type": "appointments_reminder" },
"center": { "id": "4c7a91be-2d05-4e37-88ab-1f60c9d4e2b8", "name": "Sant Jordi Gràcia", "externalId": "CEN-05" },
"call": {
"id": "f18e2a67-90b3-4c11-8d75-6ea3b0c94d52",
"direction": "outbound",
"channel": "phone",
"modality": "voice",
"startedAt": "2026-09-01T17:21:40.000+02:00",
"endedAt": "2026-09-01T17:21:40.000+02:00",
"durationSecs": 0,
"outcome": "no_answer",
"endedBy": null,
"phone": { "from": "+34932556677", "to": "+34611223344", "patient": "+34611223344" },
"patient": null,
"transfer": null,
"actions": [],
"transcript": []
}
}
}note.created
{
"event": "note.created",
"eventId": "evt_c908551da2eadcc8599912ea",
"version": 1,
"sentAt": "2026-09-01T11:39:00.380+02:00",
"attempt": 1,
"data": {
"note": {
"id": "f4d83dde-f150-4891-b8cb-d42928e9cfbe",
"createdAt": "2026-09-01T11:38:57.785+02:00",
"callId": "3ab6f0d2-59c4-4f0e-9a71-2b8d5c6e1f10",
"patientName": "Marta Ruiz",
"phone": "+34600112233",
"callReason": "Pregunta si el seguro cubre la prueba de esfuerzo. Prefiere que la llamen por la tarde.",
"fields": [
{ "key": "customField1", "label": "Aseguradora", "value": "Sanitas" },
{ "key": "customField2", "label": "Franja preferida", "value": "Tarde" },
{ "key": "customField3", "label": "Nº de póliza", "value": null }
]
},
"channel": "voice",
"agent": { "id": "7c1f04d8-3b62-4a19-8f30-9d1e2c7a5b44", "name": "Recepción Sant Jordi", "type": "appointments_booking" },
"center": { "id": "b81e5a2c-7f14-4d80-9c66-3ab0e1d95f27", "name": "Sant Jordi Eixample", "externalId": "CEN-02" }
}
}