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, contactá al equipo de soporte técnico.
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 | ✅ |
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 cambia ninguno de los endpoints que ya consumís: crear centros hijos, pedir credenciales, listar centros, consultar doctores y licencias siguen funcionando igual. 👉 Ver API Reference
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 asientos.
{
"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 usás en la API de licencias). 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 grabaciones. |
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 ISO-8601 | No | Solo queda registrado. |
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. Planes admitidos en planType: BASIC y ELITE (TRIAL no se admite: los trials se
crean al crear el centro hijo).
1️ checkout.session.completed — el centro contrató
Se envía cuando un centro contrató y el partner ya cobró el primer período.
{
"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[].planType | Sí | BASIC o ELITE. 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. |
Qué hace Speaknosis: crea la suscripción y una licencia por cada unidad (4 en el ejemplo). Quedan sin asignar, y el administrador del centro las asigna a sus médicos desde el portal. Si el centro tiene un único médico, se le asigna automáticamente.
La cantidad es absoluta, no un delta. Reenviar el alta con la misma cantidad no agrega asientos: reconcilia.
| 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 quedó en error o sin terminar | Se reprocesa |
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.
{
"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 asientos
data.items[].quantity es la cantidad total que debe quedar, no el delta.
- Si sube (de 3 a 5): se crean 2 licencias nuevas sin asignar. Nadie pierde nada. Si el centro tiene un solo médico sin licencia viva, el asiento nuevo se le asigna solo y se lo rehabilita.
- Si baja (de 5 a 3): se liberan 2, en este orden hasta cubrir la diferencia:
- Las licencias libres, sin médico asignado.
- Si no alcanzan y se envió
scheduledRemovals: las de esos médicos. - Si no alcanzan y no se envió: las de menor uso en el período primero.
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. Son los mismos IDs
que devuelve GET /payments/v1/subscription/provider/{providerId}/user en doctors[].id. Si alguno no
pertenece a ese centro, la respuesta es 404 y no se aplica nada. Solo aplica cuando baja la
cantidad.
b. Actualizar datos del contrato
Enviando renewsAt, holderName o billingEmail 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.
⚠️ Sin
reactivate, unupdatedsobre un centro dado de baja actualiza los datos y 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.
Un detalle: si algún médico había agotado su tope de grabaciones antes de la baja, vuelve deshabilitado — se habilita solo cuando arranca el período siguiente. Los demás vuelven con acceso normal.
3️ customer.subscription.deleted — baja del servicio
Sin data.
{
"type": "customer.subscription.deleted",
"id": "5a0e8c71-9d34-472b-b158-6f3a0c94e2d8",
"providerId": 42
}
Qué hace Speaknosis: deja la suscripción inactiva y deshabilita las licencias y los médicos — el acceso se corta.
No se borra nada. Se conservan la suscripción, 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.
ℹ️ Si un centro pide el borrado real de sus datos, se coordina por correo caso por caso. El webhook no borra datos.
El tope de grabaciones y su renovación
Los planes BASIC tienen un tope de grabaciones por período (100 por defecto). Cuando un médico lo alcanza, su licencia se deshabilita hasta que arranca el período siguiente.
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. - 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, sin importar si el centro está al día: Speaknosis no tiene esa información. Un centro atrasado sigue trabajando hasta que llegue el evento de baja.
Respuestas y errores
{
"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, type desconocido, id que no es UUID, quantity fuera de rango | No — hay que corregir el evento |
| 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, o un doctorId de scheduledRemovals no pertenece a 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: un updated/deleted sin contrato previo, o un alta sobre un centro dado de baja | Sí |
| 500 | Error de Speaknosis | Sí |
Formato de error:
{
"success": false,
"code": 400,
"error": {
"code": 400,
"errors": ["data.items[0].quantity must be an integer >= 1"]
}
}
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ó, reenvialo: 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:
- Generalo una sola vez, cuando ocurre el cambio, y reusalo en todos los reintentos de ese mismo cambio. Un UUID nuevo por reintento rompe la idempotencia y el efecto se aplica dos veces.
- Usá un
iddistinto por cada cambio. Reutilizarlo para dos operaciones distintas hace que la segunda se ignore como duplicada. - Un evento que falló con
422o500no cuenta como procesado: se puede reintentar con el mismoid.
Reintentos
Para 422 y 500 se recomienda backoff exponencial: 1 min, 5 min, 30 min, 2 h, 6 h, y después
abandonar con alerta al equipo.
El caso de 422 más probable es el orden: un customer.subscription.updated que llega antes que el
checkout.session.completed del mismo centro devuelve 422 hasta que exista el contrato. El reintento
lo resuelve solo.
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).
Eventos salientes en este régimen
El webhook saliente de Speaknosis hacia el partner sigue funcionando y confirma el efecto de cada evento entrante:
| Evento saliente | Cuándo |
|---|---|
subscription.contracted | Al procesarse un checkout.session.completed |
subscription.updated | Al procesarse un customer.subscription.updated que cambia cantidades |
license.associated / license.dissociated | Cuando se asignan o desasignan médicos, desde el portal o por autoasignación |