Webhooks — Eventos en tiempo real
Speaknosis envía eventos en tiempo real al endpoint configurado en tu sistema cada vez que ocurre un cambio relevante en las suscripciones o licencias de tus providers hijos.
💡 Configuración: El endpoint receptor debe ser acordado y registrado con el equipo de Speaknosis antes de comenzar. Contacta a soporte técnico para configurarlo.
↔️ Estos son los eventos que Speaknosis te envía a ti. Si tu provider opera en régimen de self-billing, además existe un webhook en sentido inverso —de tu sistema hacia Speaknosis— para notificar altas, cambios de cantidad y bajas. 👉 Ver Self-billing
Estructura general del payload
Todos los eventos comparten la misma estructura base:
{
"type": "nombre.del.evento",
"message": {
"providerId": 1042,
...
}
}
| Campo | Tipo | Descripción |
|---|---|---|
type | string | Identificador del tipo de evento. |
message | object | Cuerpo del evento. Varía según el tipo. |
providerId | number | ID interno del provider que originó el evento en Speaknosis. |
Eventos disponibles
| Evento | Cuándo se dispara |
|---|---|
subscription.contracted | El provider completa el pago de un plan por primera vez (o upgrade desde trial) |
subscription.updated | El provider cambia la cantidad de licencias, o se aplica un downgrade al renovarse |
subscription.cancelled | El provider cancela la suscripción (se hará efectiva al fin del período actual) |
license.associated | Se asignan uno o más usuarios a licencias |
license.dissociated | Se remueven uno o más usuarios de sus licencias |
Qué eventos se disparan en cada régimen de facturación
| Evento | STRIPE | PARTNER_SELF (self-billing) |
|---|---|---|
subscription.contracted | Al confirmarse el pago en Stripe | Al procesarse tu checkout.session.completed |
subscription.updated | Al cambiar cantidades desde el portal, o al aplicarse un downgrade programado | Al procesarse tu customer.subscription.updated con items (aunque las cantidades no cambien), y en los casos de consumo de la tabla de abajo |
subscription.cancelled | Al cancelar la suscripción desde el portal | No se envía: la baja la originas tú con customer.subscription.deleted |
license.associated | ✅ | ✅ |
license.dissociated | ✅ | ✅ |
En self-billing, subscription.contracted y subscription.updated funcionan como confirmación de
que el evento que enviaste se aplicó:
- Se envían mientras se procesa tu evento, así que pueden llegarte antes que la respuesta HTTP de ese evento.
- En un centro por cupos,
subscription.updatedsolo incluye los planes que venían en tu evento. Por ejemplo, si enviasteBASIC: 0, el evento trae soloBASICy no informaELITE. Para el estado completo, consultaGET .../license. - Al procesarse un alta por cupos sobre un centro en período de prueba, se envía
license.dissociatedpor los médicos que tenían licencia de prueba: esas licencias se eliminan al contratar.
Centros en modalidad por consumo
La modalidad por consumo existe por ahora solo en self-billing (👉 ver Self-billing). Para un centro en esa modalidad, los eventos se envían así:
| Evento | Cuándo se envía |
|---|---|
subscription.contracted | Al procesarse un alta con el ítem USAGE |
subscription.updated | Cuando el centro pasa de cupos a consumo, cuando cambian sus consultas incluidas, y cuando se aplica en la renovación un paso de consumo a cupos (este último, con las cantidades de cupos) |
license.associated | Cuando un médico recibe su licencia al abrir la herramienta de grabación, con plan: "USAGE" |
license.dissociated | Solo cuando se aplica un paso de consumo a cupos, por cada médico que tenía licencia. No se envía al pasar a consumo, porque ahí los médicos no pierden el acceso |
🟢 subscription.contracted
Se dispara cuando el provider completa el pago de un plan por primera vez.
{
"type": "subscription.contracted",
"message": {
"providerId": 1042,
"licenses": [
{ "plan": "BASIC", "total": 5 },
{ "plan": "ELITE", "total": 3 }
]
}
}
| Campo | Tipo | Descripción |
|---|---|---|
licenses | array | Lista de planes contratados con su cantidad total de cupos. |
licenses[].plan | string | Nombre del plan: BASIC, ELITE o USAGE (modalidad por consumo). |
licenses[].total | number | null | Cantidad total de cupos contratados para ese plan. null en USAGE, porque no hay cupos. |
licenses[].includedConsultations | number | Solo en USAGE: consultas incluidas en el mínimo mensual. |
Centro en modalidad por consumo:
{
"type": "subscription.contracted",
"message": {
"providerId": 1042,
"licenses": [{ "plan": "USAGE", "total": null, "includedConsultations": 300 }]
}
}
⚠️ Si tu sistema lee
totalcomo un número, tiene que aceptarnullpara los centros por consumo.
🔵 subscription.updated
Se dispara cuando el provider modifica la cantidad de licencias contratadas. También se dispara al inicio de un período de renovación cuando hay un downgrade programado que se aplica.
ℹ️ En renovaciones simples (sin cambio de cantidad) no se envía este evento.
{
"type": "subscription.updated",
"message": {
"providerId": 1042,
"licenses": [
{ "plan": "BASIC", "total": 8 },
{ "plan": "ELITE", "total": 8 }
]
}
}
| Campo | Tipo | Descripción |
|---|---|---|
licenses | array | Lista de planes con su cantidad total vigente tras el cambio (no el delta). En self-billing por cupos, solo los planes que venían en tu evento. |
licenses[].plan | string | Nombre del plan: BASIC, ELITE o USAGE (modalidad por consumo). |
licenses[].total | number | null | Nueva cantidad total contratada para ese plan. null en USAGE. |
licenses[].includedConsultations | number | Solo en USAGE: consultas incluidas vigentes. Un cambio rige desde el próximo período. |
En un centro por consumo, este evento llega con { "plan": "USAGE", "total": null, "includedConsultations": 500 } cuando el centro pasa a consumo o cambian sus consultas incluidas. Cuando
se aplica un paso de consumo a cupos, llega con las cantidades de cupos, igual que en el flujo de cupos.
🟠 subscription.cancelled
Se dispara cuando el provider cancela la suscripción. La cancelación no es inmediata: se hace efectiva al final del período de facturación actual (hasta esa fecha los doctores mantienen su acceso).
{
"type": "subscription.cancelled",
"message": {
"providerId": 1042,
"cancelAt": "2026-09-01 00:00:00"
}
}
| Campo | Tipo | Descripción |
|---|---|---|
cancelAt | string | Fecha y hora (UTC) en que la cancelación se hace efectiva (fin del período actual). |
🟡 license.associated
Se dispara cuando se asignan uno o más usuarios a licencias. Puede incluir múltiples usuarios en un solo evento si la asignación fue en lote.
{
"type": "license.associated",
"message": {
"providerId": 1042,
"users": [
{ "userId": "tu-id-externo-1", "plan": "BASIC" },
{ "userId": "tu-id-externo-2", "plan": "ELITE" }
]
}
}
| Campo | Tipo | Descripción |
|---|---|---|
users | array | Lista de usuarios que recibieron una licencia. |
users[].userId | string | ID externo del usuario en tu sistema (el id enviado al registrar el doctor). |
users[].plan | string | Plan asignado: BASIC, ELITE, TRIAL o USAGE. |
ℹ️ En un centro por consumo, la licencia se crea cuando el médico abre la herramienta de grabación por primera vez, y el evento llega con
plan: "USAGE".
🔴 license.dissociated
Se dispara cuando se remueve uno o más usuarios de sus licencias. Puede incluir múltiples usuarios en un solo evento si la remoción fue en lote.
{
"type": "license.dissociated",
"message": {
"providerId": 1042,
"users": [{ "userId": "tu-id-externo-1" }, { "userId": "tu-id-externo-2" }]
}
}
| Campo | Tipo | Descripción |
|---|---|---|
users | array | Lista de usuarios que perdieron su licencia. |
users[].userId | string | ID externo del usuario en tu sistema. |
ℹ️ Este evento no incluye el plan que tenía el usuario, ya que la licencia fue liberada.
ℹ️ En un centro por consumo, este evento solo se envía cuando se aplica un paso a cupos: los médicos quedan sin licencia hasta que el administrador les asigna un cupo. Al pasar a consumo no se envía, porque los médicos no pierden el acceso.
Consideraciones de implementación
- Tu endpoint debe responder con
HTTP 2xxpara confirmar la recepción. - En self-billing, la confirmación de un evento que enviaste puede llegarte antes que la respuesta HTTP de ese evento. No dependas de ese orden.
- El endpoint debe estar disponible públicamente y soportar solicitudes
POSTconContent-Type: application/json. - Si el endpoint no responde o devuelve un error, Speaknosis no reintenta el envío automáticamente.
- El campo
providerIdsiempre hace referencia al provider que originó el evento, útil si un mismo endpoint recibe eventos de múltiples providers hijos.