Self-billing — facturación gestionada por el partner
En el régimen de self-billing, el provider padre (el partner) le cobra directamente a sus centros con su propia facturación, y le notifica a Speaknosis el resultado mediante un webhook. Speaknosis sigue administrando las licencias, los usuarios habilitados y el acceso al producto.
💡 Este régimen se habilita a nivel del provider padre, no centro por centro: cuando se activa, todos sus centros hijos —los actuales y los que se creen después— pasan al flujo nuevo. Para habilitarlo, contacta al equipo de soporte técnico.
Términos que usa esta página:
| Término | Qué es |
|---|---|
| Cupo | Unidad contratada de un plan BASIC o ELITE. Un cupo permite asignar una licencia a un médico. |
| Licencia | Lo que tiene asignado cada médico para poder usar la herramienta. |
| Consulta | Cada atención que el médico graba. Es lo que se mide en la modalidad por consumo (USAGE): cuenta una vez, aunque su informe se regenere. |
| Informe | Cada generación del informe de una consulta, incluidas las regeneraciones. Es lo que mide el tope de BASIC. |
Una consulta cuyo informe se regeneró dos veces suma 3 informes al tope de BASIC y 1 consulta
al consumo.
Reparto de responsabilidades
| Partner (provider padre) | Speaknosis | |
|---|---|---|
| Definir la fecha de renovación del centro | ✅ | |
| Emitir la factura y cobrar al centro | ✅ | |
| Reintentar un cobro fallido | ✅ | |
| Decidir cuándo un impago es definitivo y enviar la baja | ✅ | |
| Recibir el pedido de un centro que quiere contratar o cambiar de plan | ✅ | |
| Avisarle al centro sobre su pago, deuda o suspensión | ✅ | |
| Notificar altas, cambios de cantidad y bajas | ✅ | |
| Crear y administrar las licencias | ✅ | |
| Habilitar y deshabilitar usuarios | ✅ | |
| Cortar y reactivar el acceso cuando el partner lo indica | ✅ | |
| Asignar licencias a médicos | ✅ | |
| Acordar con Speaknosis el mínimo mensual y el precio del excedente (modalidad por consumo) | ✅ | ✅ |
| Informar las consultas incluidas en el mínimo de cada centro (modalidad por consumo) | ✅ | |
| Contar las consultas de cada período (modalidad por consumo) | ✅ |
En una frase: el partner controla el cobro; Speaknosis controla el acceso. Cada evento del webhook es la orden que traduce una decisión de cobro en un efecto sobre el acceso.
Qué cambia y qué no
No cambian las rutas de los endpoints que ya consumes: crear centros hijos, pedir credenciales, listar centros, consultar doctores y licencias siguen funcionando igual. 👉 Ver API Reference
Si tus centros venían del régimen STRIPE, esto es lo que cambia en lo que ya usabas:
STRIPE | PARTNER_SELF (self-billing) | |
|---|---|---|
Abrir un checkout para un centro (POST .../checkoutSession) | Abre la sesión de pago | Responde 409: la contratación va por el webhook del partner |
GET .../provider/{providerId}/user | Trae suspension y billing.mode: "STRIPE" | No trae suspension; billing.mode es "PARTNER_SELF" y billing.managedBy el nombre del partner |
GET .../provider/{providerId}/license (resumen) | Trae scheduledDowngrade y suspension | No trae ninguno de los dos; trae status: "ACTIVE" | "INACTIVE" |
GET .../provider/{providerId}/license?userId=... | Trae suspension | No trae suspension |
| Cambios de cantidad | Una reducción se aplica en el próximo período | Se aplican de inmediato (salvo el paso de consumo a cupos) |
Webhook saliente subscription.cancelled | Se envía al cancelar desde el portal | No se envía: la baja la originas tú con customer.subscription.deleted |
| Pago fallido y suspensión | Automáticos, con aviso por email | No existen: el acceso solo se corta con la baja |
Si tu integración lee suspension o scheduledDowngrade, trata su ausencia como "sin corte" y "sin
downgrade programado".
Cambia el canal de contratación:
- El tab de contratación del Portal de Gestión de Licencias se oculta para los centros del partner. Contratar, cambiar cantidad y cancelar dejan de estar disponibles ahí; en su lugar el portal muestra un mensaje que deriva al partner.
- Lo que sigue igual en el portal: ver licencias, uso por médico, y asignar y desasignar médicos. Es el uso diario del administrador del centro y no se toca.
- El partner necesita un canal propio para que sus centros pidan licencias (su propio panel, un pedido a soporte, lo que definan).
- Los avisos de cobro y suspensión por falta de pago desaparecen para estos centros: no hay estado intermedio de "pago pendiente" ni suspensión automática. El único camino que corta el acceso es el evento de baja.
El webhook
👨🏻💻 QA:
POST https://api-qa.speaknosis.com/payments/v1/partner/webhook
🏥 Producción:
POST https://api-prod.speaknosis.com/payments/v1/partner/webhook
Headers:
| Header | Obligatorio | Descripción |
|---|---|---|
Authorization: Bearer <token> | Sí | Token M2M (client_credentials). Ver Autenticación |
Content-Type: application/json | Sí | El cuerpo del evento se envía siempre como JSON. |
El token debe ser dueño del providerId del cuerpo, o de su provider padre. Un centro que no cuelgue
del partner recibe 403.
Envelope
Todos los eventos comparten esta estructura. Los campos de contrato van afuera de data; data
queda para lo que describe el cambio de cupos.
{
"type": "checkout.session.completed",
"id": "9f8b2c14-5d3e-4a7b-9c21-6f0e8d4a1b33",
"providerId": 42,
"renewsAt": "2026-09-05T00:00:00Z",
"holderName": "Clínica Andes SpA",
"externalProviderId": "rsv_ctr_998",
"data": {}
}
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
type | string | Sí | Uno de los 3 tipos de abajo. Cualquier otro valor → 400. |
id | string (UUID) | Sí | Identificador único del evento en tu sistema. Es la clave de idempotencia. |
providerId | number | Sí | ID del centro médico en la numeración de Speaknosis (el mismo que usas en la API de licencias). Entero positivo. No es el ID del partner. |
renewsAt | string ISO-8601 | En el alta | Fecha de la próxima renovación del centro. Gobierna cuándo se reinicia el tope de informes y cuándo se corta cada período de consumo. Ver las reglas de abajo. |
holderName | string | En el alta | Razón social o titular. Solo se guarda. |
billingEmail | string | En el alta | Correo de finanzas del centro. Solo se guarda; Speaknosis no le envía mails. |
externalProviderId | string | No | ID del centro en el sistema del partner. Se guarda para conciliación y soporte. |
reactivate | boolean | No | Solo en customer.subscription.updated. Reactiva un centro dado de baja. |
occurredAt | string | No | Solo queda registrado. No se usa para ordenar eventos (ver Orden de los eventos). |
data | object | Depende | Según el evento. El de baja no lo lleva. |
renewsAt, holderName y billingEmail son obligatorios en el alta y opcionales en la
actualización. Los strings no pueden venir vacíos. Planes admitidos en planType: BASIC, ELITE
y USAGE (sin distinguir mayúsculas), donde USAGE es la modalidad por consumo (👉 ver más
abajo). TRIAL no se admite: los trials se crean al crear el centro hijo.
Reglas para renewsAt:
- Envíalo en UTC, con la
Zfinal:2026-09-05T00:00:00Z. Los cortes de período y el día de renovación se calculan en UTC. - Debe ser la próxima fecha de renovación: una fecha futura, dentro del mes siguiente.
- El ciclo es mensual y conserva el día y la hora de la fecha que envías.
1️⃣ checkout.session.completed — el centro contrató
Se envía cuando un centro contrató y el partner ya cobró el primer período.
ℹ️ Esta sección describe el alta por cupos. Para contratar por consumo (
USAGE) los campos dedata.itemscambian: no llevaquantityy síincludedConsultations. 👉 Ver Contratar por consumo
{
"type": "checkout.session.completed",
"id": "9f8b2c14-5d3e-4a7b-9c21-6f0e8d4a1b33",
"providerId": 42,
"renewsAt": "2026-09-05T00:00:00Z",
"holderName": "Clínica Andes SpA",
"externalProviderId": "rsv_ctr_998",
"data": {
"items": [
{ "planType": "BASIC", "quantity": 3 },
{ "planType": "ELITE", "quantity": 1 }
]
}
}
| Campo | Obligatorio | Notas |
|---|---|---|
data.items | Sí | Array no vacío. |
data.items[].planType | Sí | BASIC o ELITE (para USAGE, ver Contratar por consumo). No se puede repetir el mismo plan en dos items. |
data.items[].quantity | Sí | Entero ≥ 1. 0 devuelve 400 — para cortar el acceso se usa el evento 3. |
data.items[].includedConsultations | No se admite | Solo existe en USAGE. Enviarlo con BASIC o ELITE devuelve 400. |
Qué hace Speaknosis:
- Crea el contrato y un cupo por cada unidad (4 en el ejemplo), sin asignar. El administrador del centro los asigna a sus médicos desde el portal.
- Si el centro estaba en período de prueba, elimina sus licencias de prueba. Los médicos que las
tenían quedan sin licencia hasta que el administrador les asigna un cupo, y se envía
license.dissociatedpor ellos. - Envía
subscription.contractedcon las cantidades contratadas. - Si el centro tiene un único médico, le asigna un cupo automáticamente y envía
license.associated.
La cantidad es absoluta, no un delta. Si un alta falla y la reenvías con el mismo id, no se
duplican cupos.
| Situación | Respuesta |
|---|---|
El centro ya tiene un contrato activo y llega un alta con un id nuevo | 409 — el cambio va por customer.subscription.updated |
El centro está dado de baja (INACTIVE) | 422 — se reactiva con customer.subscription.updated + reactivate: true |
Mismo id que antes respondió con error | Se reprocesa |
Mismo id que ya se procesó | 200 con status: "duplicate" |
El resultado se puede verificar con GET /payments/v1/subscription/provider/{providerId}/license.
2️⃣ customer.subscription.updated — actualización parcial
Es un PUT parcial: lo que no se envía, no se toca. Sirve para tres cosas, combinables en un mismo evento.
ℹ️ Esta sección describe un centro por cupos. Para pasar un centro a consumo, cambiar sus consultas incluidas o volver de consumo a cupos,
data.itemslleva otros campos y hay reglas propias. 👉 Ver Cambios sobre un centro por consumo
{
"type": "customer.subscription.updated",
"id": "c41d7a90-2b6e-4f18-8ad3-7e5c1908b2f4",
"providerId": 42,
"renewsAt": "2026-10-20T00:00:00Z",
"data": {
"items": [{ "planType": "BASIC", "quantity": 5 }],
"scheduledRemovals": ["doctor-ext-2", "doctor-ext-7"]
}
}
a. Cambiar la cantidad de cupos
data.items[].quantity es la cantidad total que debe quedar, no el delta. Entero ≥ 0.
-
Si sube (de 3 a 5): se crean 2 cupos nuevos sin asignar. Nadie pierde nada. Si el centro tiene un solo médico sin licencia, el cupo nuevo se le asigna solo y se lo rehabilita.
-
Si baja (de 5 a 3): se liberan 2 cupos de ese plan, en este orden hasta cubrir la diferencia:
- Los cupos libres, sin médico asignado.
- Si no alcanzan y se envió
scheduledRemovals: los de esos médicos. - Si todavía no alcanzan: los de los médicos con menos informes en el período primero.
Los médicos que pierden su cupo quedan sin acceso. Para saber qué médicos conservan su licencia después de una reducción, consulta
GET /payments/v1/subscription/provider/{providerId}/user. -
quantity: 0da de baja ese plan: se liberan todos sus cupos con el mismo orden. Los planes que no vienen enitemsquedan intactos — así un centro cambia de plan conservando el otro (por ejemploBASIC: 0+ELITE: 1).
data.scheduledRemovals es un array de doctorId que deben perder la licencia cuando baja la
cantidad. Son los mismos IDs que devuelve GET /payments/v1/subscription/provider/{providerId}/user en
doctors[].id. Reglas:
- Cada médico de la lista debe tener una licencia asignada en ese centro. Si alguno no la tiene, la
respuesta es
404y no se aplica nada. - Solo se usan para cubrir la reducción del plan al que pertenece la licencia de cada médico. Si la lista tiene más médicos que cupos a liberar, no se garantiza cuáles de ellos pierden la licencia: envía exactamente los que deben salir.
b. Actualizar datos del contrato
Enviando renewsAt, holderName, billingEmail o externalProviderId se actualiza ese campo y
nada más. Un evento con solo holderName no toca ninguna licencia. Un renewsAt nuevo
reprograma la renovación desde esa fecha.
c. Reactivar un centro dado de baja
Requiere reactivate: true en el envelope:
{
"type": "customer.subscription.updated",
"id": "7c3f1a08-4e52-4d9b-91a6-2b8d05f7c134",
"providerId": 42,
"reactivate": true,
"renewsAt": "2026-11-05T00:00:00Z",
"data": { "items": [{ "planType": "BASIC", "quantity": 3 }] }
}
El centro vuelve con sus licencias y las asignaciones de médicos tal como estaban. No hay que
reconfigurar nada. reactivate: true sobre un centro activo no tiene efecto.
⚠️ Sin
reactivate, unupdatedsobre un centro dado de baja no devuelve el acceso. Es deliberado: permite corregir la razón social o el correo de un centro que se fue, sin reactivarlo por accidente. Sobre un centro dado de baja, envía solo datos de contrato; los cambios de cantidad, envíalos junto conreactivate: true.
Un detalle: si algún médico había agotado su tope de informes antes de la baja, vuelve deshabilitado — se habilita cuando se reinicia el tope en la siguiente renovación. Los demás vuelven con acceso normal.
3️⃣ customer.subscription.deleted — baja del servicio
Sin data.items.
{
"type": "customer.subscription.deleted",
"id": "5a0e8c71-9d34-472b-b158-6f3a0c94e2d8",
"providerId": 42
}
Qué hace Speaknosis: deja el contrato inactivo y deshabilita las licencias y los médicos — el acceso se corta. Si el centro ya estaba dado de baja, no hace nada.
No se borra nada. Se conservan el contrato, las licencias y qué médico tenía cada una, justamente
para que un updated posterior reactive el centro sin trabajo de configuración.
Este es el único evento que corta el acceso, y sirve para los tres casos sin distinguirlos: impago definitivo, el centro se va del partner, o el centro da de baja solo el módulo.
⚠️ La baja actúa sobre el estado actual del centro, también si todavía está en período de prueba: corta el acceso de prueba, y un alta posterior responde
422hasta que reactives el centro. Por eso, envía siempre primero el alta (ver Orden de los eventos).
ℹ️ Si un centro pide el borrado real de sus datos, se coordina por correo caso por caso. El webhook no borra datos.
Modalidad por consumo
Además de los cupos BASIC y ELITE, un centro puede contratar la modalidad por consumo. Está
pensada para centros grandes, donde comprar y asignar un cupo por cada médico no escala:
- El centro no compra cupos. Todos sus médicos pueden grabar, sin tope de médicos ni de consultas.
- Lo que se mide es la cantidad de consultas del período.
- El centro paga un mínimo mensual que incluye una cantidad de consultas (las consultas incluidas). Las consultas que la superan son excedente.
- El monto del mínimo y el precio del excedente se acuerdan con Speaknosis y no se envían por el webhook. El partner solo informa la cantidad de consultas incluidas.
- Un centro tiene una sola modalidad a la vez: cupos (
BASICy/oELITE) o consumo (USAGE).
Qué consultas se cuentan
- Cada consulta pertenece al período en el que empezó, es decir, en el que el médico la inició.
- Una consulta cuenta si su informe está generado al cierre del período (ver Períodos de consumo). Cuenta una sola vez, aunque el informe se regenere.
- No cuenta una consulta que al cierre no tiene su informe generado: con error, pausada, sin terminar o todavía en proceso. Tampoco cuenta en el período siguiente, porque empezó en el anterior. Por ejemplo, una consulta iniciada a las 23:50 del último día cuenta solo si su informe ya está generado cuando se cierra ese período.
- Las consultas borradas después de generar su informe cuentan igual.
Períodos de consumo
- Los períodos siguen el
renewsAtdel centro: son mensuales y conservan el día y la hora de la fecha original, en UTC. - Mientras el contrato está activo, los períodos son contiguos: cada uno empieza donde terminó el anterior. Entre una baja y una reactivación no hay período (ni acceso ni consumo).
- El primer período va desde el momento del alta (o del paso a consumo) hasta el
renewsAt, y lleva las consultas incluidas completas, aunque dure menos de un mes. - Cada período guarda las consultas incluidas vigentes al empezar. Un cambio de incluidas rige desde el período siguiente, sin efectos retroactivos.
- El cierre de un período lo hace un proceso diario que corre a las 00:00 UTC: ocurre dentro de las 24 horas siguientes a su fecha de renovación. También se cierra en el momento en que llega una baja. Al cerrarse, su número de consultas queda fijo: lo que cambie después no lo modifica.
- De un período cerrado se obtienen sus consultas incluidas, las consumidas y el excedente (las consumidas por encima de las incluidas, o 0 si no las superan).
ℹ️ En esta etapa no hay un endpoint ni un evento para consultar el consumo de un período. Speaknosis lo obtiene al cierre de cada período y lo comparte con el partner para la facturación.
Estados de un centro
| Transición | Evento | Cuándo rige |
|---|---|---|
| Alta por cupos o por consumo | checkout.session.completed con BASIC/ELITE o con USAGE | De inmediato |
| Cupos → Consumo | customer.subscription.updated con USAGE | De inmediato |
| Consumo → Paso a cupos pendiente | customer.subscription.updated con BASIC/ELITE (suma de quantity mayor que 0) | Se programa para la próxima renovación |
| Paso pendiente → Consumo | customer.subscription.updated con USAGE: cancela el paso | De inmediato |
| Paso pendiente → Cupos | Renovación del centro: se aplica el paso | En la renovación |
| Cualquiera → Inactivo | customer.subscription.deleted. Si había un paso a cupos pendiente, se descarta | De inmediato |
| Inactivo → la modalidad que tenía | customer.subscription.updated con reactivate: true | De inmediato |
Sin cambiar de estado:
- Cupos: un
updatedconBASIC/ELITEcambia las cantidades. - Consumo: un
updatedconUSAGEcambia las consultas incluidas desde el próximo período. - Paso a cupos pendiente: un nuevo
updatedconBASIC/ELITEreemplaza el paso programado.
Desde Inactivo, el centro vuelve a la modalidad que tenía. Para cambiarle la modalidad, primero reactívalo y después envía el cambio en otro evento.
Contratar por consumo
Se usa el mismo checkout.session.completed, con un único ítem USAGE. Para contratar por primera vez
—también desde el período de prueba— usa siempre el alta, no un updated.
{
"type": "checkout.session.completed",
"id": "0192f1c4-7a3b-7e21-9d0a-5b6c7d8e9f01",
"providerId": 42,
"renewsAt": "2026-11-01T00:00:00Z",
"holderName": "Clínica Grande SpA",
"data": {
"items": [{ "planType": "USAGE", "includedConsultations": 300 }]
}
}
| Campo | Obligatorio | Notas |
|---|---|---|
data.items[].planType | Sí | USAGE. Tiene que ser el único ítem: no se combina con BASIC ni ELITE. |
data.items[].includedConsultations | Sí | Consultas incluidas en el mínimo mensual. Entero ≥ 0 (0 es válido). |
data.items[].quantity | No se admite | En consumo no hay cupos. Enviarlo devuelve 400. |
data.scheduledRemovals | No se admite | Enviarlo junto con un ítem USAGE devuelve 400. |
Qué hace Speaknosis:
- Deja el contrato activo en modalidad por consumo, con las consultas incluidas informadas.
- Elimina las licencias de prueba del centro. Los médicos no quedan bloqueados: cada uno recibe su
licencia de consumo la próxima vez que abre la herramienta. Por eso no se envía
license.dissociated. - Abre el primer período de consumo, desde el momento del alta hasta el
renewsAt. - Envía
subscription.contracted:
{
"type": "subscription.contracted",
"message": {
"providerId": 42,
"licenses": [{ "plan": "USAGE", "total": null, "includedConsultations": 300 }]
}
}
No hay que asignar licencias. Cada médico recibe su licencia automáticamente la primera vez que abre
la herramienta de grabación, también los médicos que se creen después. Esa asignación se notifica con
license.associated y el plan USAGE. Un médico sigue grabando aunque el centro ya haya superado sus
consultas incluidas.
Para verificar el alta, GET /payments/v1/subscription/provider/{providerId}/license devuelve:
{
"success": true,
"code": 200,
"response": {
"contractModel": "USAGE",
"summary": [{ "type": "USAGE", "total": null, "used": 0, "available": null }],
"scheduledSwitch": null,
"status": "ACTIVE"
}
}
used es la cantidad de médicos que ya recibieron su licencia de consumo.
Cambios sobre un centro por consumo
Los cambios de modalidad y de consultas incluidas se envían con customer.subscription.updated:
| Estado del centro | data.items | Efecto | Cuándo rige |
|---|---|---|---|
| Por cupos | [{ "planType": "USAGE", "includedConsultations": 300 }] | Pasa a consumo. Se eliminan sus licencias de cupos (sin license.dissociated) y cada médico recibe su licencia de consumo al abrir la herramienta de grabación. Se envía subscription.updated con el ítem USAGE. | De inmediato |
| Por consumo | [{ "planType": "USAGE", "includedConsultations": 500 }] | Cambia las consultas incluidas y se envía subscription.updated. Si había un paso a cupos pendiente, lo cancela. | Desde el próximo período |
| Por consumo | [{ "planType": "ELITE", "quantity": 10 }] (o BASIC, o los dos) | Programa el paso a cupos. Hasta la renovación el centro sigue por consumo. No se envía ningún evento hasta que se aplica. | En la próxima renovación |
| Por consumo | Sin items | Solo actualiza datos del contrato. Un renewsAt nuevo mueve el fin del período en curso y la fecha del paso a cupos pendiente, si lo hay. | De inmediato |
💡 Al pasar a consumo un centro por cupos, incluye
renewsAten el evento si cambió su fecha de renovación. Las consultas incluidas del primer período son las del evento.
El paso a cupos pendiente aparece en GET .../license como scheduledSwitch:
{
"success": true,
"code": 200,
"response": {
"contractModel": "USAGE",
"summary": [{ "type": "USAGE", "total": null, "used": 15, "available": null }],
"scheduledSwitch": {
"quantity": { "9": 10 },
"effectiveDate": "2026-12-01 00:00:00"
},
"status": "ACTIVE"
}
}
quantity usa el ID del plan como clave (8 = BASIC, 9 = ELITE). effectiveDate es la próxima
renovación, en UTC, con formato AAAA-MM-DD HH:MM:SS.
Cuando se aplica el paso a cupos (dentro de las 24 horas siguientes a la fecha de renovación):
- se cierra el último período de consumo en el momento en que se aplica el cambio. Hasta ese momento el centro sigue por consumo, y las consultas de ese tramo cuentan en ese último período;
- se eliminan las licencias de consumo, y se envía
license.dissociatedpor cada médico que tenía una; - se crean los cupos, y se envía
subscription.updatedcon las cantidades; - desde ahí rige el flujo de cupos: el administrador del centro asigna los cupos desde el portal, salvo que el centro tenga un único médico, al que se le asigna automáticamente.
Para cancelar un paso a cupos pendiente, vuelve a enviar el ítem USAGE antes de la fecha de
renovación. Si llegan varios pasos a cupos en el mismo período, rige el último: reemplaza por completo
al anterior, no se suman.
Restricciones en un centro por consumo:
- La suma de
quantityde un paso a cupos tiene que ser mayor que 0.[{ "planType": "ELITE", "quantity": 0 }]devuelve400: para terminar el contrato se usacustomer.subscription.deleted. En un centro por cupos, en cambio, un plan en 0 sigue significando la baja de ese plan. scheduledRemovalsno se admite (400). En esta modalidad no se le quita la licencia a un médico puntual: para cortarle el acceso, el centro da de baja a ese usuario. Por el mismo motivo, la desasociación de licencias desde la API responde409.- Sobre un centro dado de baja, un cambio de modalidad devuelve
422, aunque el evento traigareactivate: true. Primero reactívalo en un evento y después envía el cambio en otro.
Baja y reactivación por consumo
La baja (customer.subscription.deleted) y la reactivación funcionan igual que en cupos, y además:
- Al dar de baja se cierra el período en curso en ese momento, y se descarta el paso a cupos pendiente, si lo había.
- Al reactivar se abre un período nuevo desde ese momento hasta la próxima renovación: el
renewsAtdel evento, si lo trae, o la siguiente fecha del ciclo del centro. - Entre la baja y la reactivación no hay acceso ni consumo.
Ejemplo: ciclo de vida de un centro por consumo
Centro 42, que nace en período de prueba al crearlo como centro hijo.
| # | Fecha (UTC) | Qué pasa | Evento que envías | Eventos que recibes |
|---|---|---|---|---|
| 1 | 05-oct 14:00 | El centro contrata por consumo con 300 incluidas. Se abre el período 05-oct → 01-nov con 300 incluidas. | checkout.session.completed con USAGE, includedConsultations: 300, renewsAt: 2026-11-01T00:00:00Z | subscription.contracted con { "plan": "USAGE", "total": null, "includedConsultations": 300 } |
| 2 | 06-oct | La Dra. Pérez abre la herramienta por primera vez y recibe su licencia. | — | license.associated con { "userId": "dra-perez", "plan": "USAGE" } |
| 3 | 20-oct | El centro sube sus incluidas a 500. El período en curso sigue con 300. | customer.subscription.updated con USAGE, includedConsultations: 500 | subscription.updated con { "plan": "USAGE", "total": null, "includedConsultations": 500 } |
| 4 | 01-nov | Renovación. Se cierra el período 1 (por ejemplo, 340 consumidas: 40 de excedente) y se abre 01-nov → 01-dic con 500 incluidas. | — | — |
| 5 | 10-nov | Impago definitivo: el partner da de baja el centro. Se cierra el período 2 en ese momento. | customer.subscription.deleted | — |
| 6 | 15-nov | El centro regulariza. Se reactiva y se abre 15-nov → 01-dic con 500 incluidas. | customer.subscription.updated con reactivate: true | — |
| 7 | 20-nov | El centro pide volver a cupos: 10 ELITE. Queda programado para el 01-dic. | customer.subscription.updated con [{ "planType": "ELITE", "quantity": 10 }] | — (GET .../license muestra scheduledSwitch) |
| 8 | 01-dic (dentro de las 24 h siguientes) | Renovación: se cierra el último período de consumo y se aplica el paso a cupos. | — | Primero license.dissociated por cada médico que tenía licencia de consumo, y después subscription.updated con { "plan": "ELITE", "total": 10 } |
| 9 | 02-dic | El administrador asigna cupos ELITE a sus médicos desde el portal. | — | license.associated con plan: "ELITE" |
El tope de informes de BASIC y su renovación
Los planes BASIC tienen un tope de 100 informes por médico y por período. Cuenta cada informe generado, incluidas las regeneraciones. Cuando un médico llega a 100, su licencia se deshabilita hasta la siguiente renovación. ELITE y USAGE no tienen tope.
Ese reinicio lo maneja Speaknosis con el renewsAt recibido en el alta:
- El ciclo es mensual, y Speaknosis avanza la fecha conservando el día de la original.
- Solo hay que enviar un
renewsAtnuevo si el centro cambia su fecha de renovación. No hace falta reenviarlo cada período. - El reinicio lo hace un proceso diario que corre a las 00:00 UTC: se aplica dentro de las 24 horas siguientes a la fecha de renovación.
- Es independiente de cómo se le factura al centro. Un centro que le paga al partner por año renueva su tope igual, todos los meses.
- Corre siempre mientras el centro esté activo, sin importar si está al día: Speaknosis no tiene esa información. Un centro atrasado sigue trabajando hasta que llegue el evento de baja.
- En la modalidad por consumo no hay tope: la misma renovación cierra un período de consumo y abre el siguiente.
Respuestas y errores
Respuesta exitosa:
{
"success": true,
"code": 200,
"response": {
"id": "9f8b2c14-5d3e-4a7b-9c21-6f0e8d4a1b33",
"status": "processed"
}
}
| Código | Significado | ¿Reintentar? |
|---|---|---|
200 processed | Procesado | No |
200 duplicate | Ese id ya se había procesado. No se aplicó de nuevo | No |
| 400 | Cuerpo inválido: falta un campo, tipo incorrecto, string vacío, type desconocido, id que no es UUID, providerId que no es entero positivo, quantity fuera de rango, plan repetido, un ítem USAGE inválido (combinado con otro plan, sin includedConsultations o con quantity), includedConsultations en un plan de cupos, un paso de consumo a cupos con cantidad total 0, o scheduledRemovals sobre un centro por consumo | No — corrige el evento. Puedes reenviarlo corregido con el mismo id |
| 401 | Token ausente, inválido o vencido | Sí, con token nuevo |
| 403 | El token no tiene permiso sobre ese providerId | No — es configuración |
| 404 | El providerId no existe, el token no corresponde a un provider registrado, o un doctorId de scheduledRemovals no tiene licencia asignada en ese centro | No |
| 409 | Ese centro no está configurado para self-billing, o llegó un alta con id nuevo sobre un contrato ya activo | No |
| 422 | Cuerpo válido pero inaplicable al estado actual del centro. Ver la tabla de abajo | Depende del caso |
| 500 | Error de Speaknosis | Sí |
Casos de 422 y qué hacer en cada uno. El texto del error describe el caso:
| Caso | Qué hacer |
|---|---|
customer.subscription.updated o .deleted sobre un centro que todavía no tiene contrato | Envía primero el alta. Reintentar el mismo evento después del alta lo resuelve |
| Alta sobre un centro dado de baja | No reintentes: reactívalo con customer.subscription.updated + reactivate: true |
| Cambio de modalidad (cupos ↔ consumo) sobre un centro dado de baja | No reintentes: reactívalo en un evento y envía el cambio en otro |
Contrato por consumo sin fecha de renovación (un centro que nunca contrató y pasa a consumo con un updated sin renewsAt) | No se aplica ningún cambio. No reintentes igual: reenvía el evento incluyendo renewsAt |
Formato de error
Los errores llegan con esta forma. Si hay varios errores de validación, vienen todos en un único
message, separados por punto y coma:
{
"error": {
"success": false,
"code": 400,
"errors": [
{
"code": 400,
"message": "id must be a UUID; data.items[0].quantity must be an integer >= 1"
}
]
},
"status": 400
}
En un 404, el detalle viene en description en lugar de message:
{
"error": {
"success": false,
"code": 404,
"errors": [{ "code": 404, "name": "NotFoundError", "description": "Health provider 42 not found" }]
},
"status": 404
}
Idempotencia
Speaknosis guarda cada id recibido, con clave (providerId, id). Reenviar un evento ya procesado
es seguro: la respuesta es 200 con status: "duplicate" y no se vuelve a aplicar el efecto. Ante la
duda de si un evento llegó, reenvíalo: es la opción segura.
El id debe ser un UUID válido (cualquier versión; se sugiere v7 por el orden temporal, pero v4
sirve igual). Lo importante:
- Genéralo una sola vez, cuando ocurre el cambio, y reúsalo en todos los reintentos de ese mismo cambio. Un UUID nuevo por reintento rompe la idempotencia y el efecto se aplica dos veces.
- Usa un
iddistinto por cada cambio. Reutilizarlo para dos operaciones distintas hace que la segunda se ignore como duplicada. - Un evento que no respondió
200no cuenta como procesado: se puede reenviar con el mismoid. Si lo reenvías corregido, se procesa el cuerpo nuevo. - No envíes el mismo evento en paralelo. Reintenta solo después de recibir la respuesta del intento anterior (o de que se venza su timeout).
Orden de los eventos
Speaknosis aplica los eventos en el orden en que los recibe. occurredAt no se usa para
reordenarlos.
- Envía los eventos de un mismo centro de a uno, y espera la respuesta de cada uno antes de enviar el siguiente.
- Envía siempre primero el alta (
checkout.session.completed). Los eventos de cambio y de baja actúan sobre el estado actual del centro, incluido su período de prueba.
Reintentos
Para 500, errores de red y timeouts, se recomienda backoff exponencial: 1 min, 5 min, 30 min, 2 h,
6 h, y después abandonar con alerta al equipo. Para 422, reintenta solo el caso de "centro sin
contrato todavía"; los demás requieren una acción (ver la tabla de arriba).
Cómo verificar el resultado
Los dos endpoints de consulta son la forma de comprobar que un evento se aplicó como se esperaba:
| Endpoint | Para qué |
|---|---|
GET /payments/v1/subscription/provider/{providerId}/license | Resumen de licencias del centro: total, en uso y disponibles por plan, más el status del contrato |
GET /payments/v1/subscription/provider/{providerId}/user | Doctores del centro con el estado de su licencia, y el billing.mode del centro |
En este régimen, GET .../license devuelve status: "ACTIVE" | "INACTIVE" en lugar de
scheduledDowngrade, y no devuelve suspension (la suspensión por falta de pago solo existe en el
flujo Stripe). Un centro en período de prueba devuelve status: "ACTIVE".
Para un centro por consumo, el resumen devuelve además contractModel: "USAGE", un único plan
USAGE con total y available en null (no hay cupos), y scheduledSwitch (null si no hay un
paso a cupos pendiente). Ver los ejemplos en Contratar por consumo y Cambios
sobre un centro por consumo.
Eventos salientes en este régimen
El webhook saliente de Speaknosis hacia el partner sigue funcionando y confirma el efecto de cada evento entrante. La confirmación se envía mientras se procesa tu evento, así que puede llegarte antes que la respuesta HTTP de ese evento. Si tu endpoint no la recibe, no se reintenta y tu evento igual queda procesado.
| Evento saliente | Cuándo |
|---|---|
subscription.contracted | Al procesarse un checkout.session.completed, también por consumo |
subscription.updated | Al procesarse un customer.subscription.updated que trae items en un centro por cupos (aunque las cantidades no cambien), que pasa un centro a consumo o que cambia sus consultas incluidas, y al aplicarse un paso de consumo a cupos en la renovación. En un centro por cupos, solo incluye los planes que venían en items |
license.associated | Cuando se asignan médicos desde el portal o por asignación automática. En consumo: cuando un médico recibe su licencia al abrir la herramienta |
license.dissociated | Cuando se desasignan médicos desde el portal, cuando un alta por cupos elimina las licencias de prueba, y en consumo, al aplicarse un paso a cupos |
Para conocer el estado completo de las licencias del centro en cualquier momento, consulta los endpoints de Cómo verificar el resultado.