Resellers: avisos del estado de tus clientes
Cuando cambia el estado de uno de tus clientes, o su suscripción adicional, REDCUMBRE le avisa a tu sistema con una llamada HTTP firmada a la dirección que configures. Los avisos son del reseller: tus clientes siguen recibiendo sus propios correos (puedes silenciarlos por suscripción adicional).
Configurar la dirección
Sección titulada «Configurar la dirección»En tu panel, en Reseller → Avisos, con el rol de acceso completo (RESELLER_FULL_ACCESS):
- Genera el secreto de firma (si todavía no lo tienes) con el botón Generar secreto: se muestra una sola vez, cópialo a tu receptor antes de cerrar la pantalla.
- Escribe la dirección
httpsdonde quieres recibir los avisos y activa el interruptor. - Guarda. Desde ese momento, el primer cambio de estado de un cliente —no el historial previo— produce el primer aviso.
Por API es la misma operación: PUT /{tenantSlug}/reseller/avisos con
{ "activo": true, "url": "https://…" }. La dirección debe ser https (si no, responde 400); activar
los avisos sin haber generado antes el secreto responde 409 AVISOS_SIN_SECRETO.
El administrador (RESELLER_ADMIN) y el lector (RESELLER_VIEWER) del reseller ven si los avisos están
activos y si hay un secreto generado, pero nunca la dirección ni el secreto; cualquier intento de escritura
de su parte responde 403.
El secreto y la verificación de la firma
Sección titulada «El secreto y la verificación de la firma»El secreto se genera sólo en el panel, nunca por API, y se muestra una sola vez. Al regenerarlo, el anterior deja de firmar en el mismo instante — a diferencia de la configuración general de webhooks, acá no hay convivencia de dos secretos ni rotación con plazo.
Cada llamada trae:
X-Webhook-Signature: t=<segundos unix>,v1=<HMAC-SHA256 hex>Content-Type: application/jsonUser-Agent: rCAPI-Webhook/1.0La firma se calcula sobre "<t>.<cuerpo crudo>", igual que el resto de los webhooks de la plataforma. El
header siempre trae un solo v1 (nunca dos, porque no hay convivencia de secretos). La verificación es la
misma lógica que la guía general de webhooks:
const crypto = require('crypto');
function validarFirma(rawBody, header, secret) { if (!header) return false; let t, v1; for (const parte of header.split(',')) { const i = parte.indexOf('='); const clave = parte.slice(0, i); const valor = parte.slice(i + 1); if (clave === 't') t = valor; if (clave === 'v1') v1 = valor; } if (!t || !v1) return false;
// Tolerancia sugerida: 5 minutos, para protegerte de un replay. if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) return false;
const esperada = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const recibida = Buffer.from(v1, 'hex'); const calculada = Buffer.from(esperada, 'hex'); return recibida.length === calculada.length && crypto.timingSafeEqual(recibida, calculada);}import hmac, hashlib, time
def validar_firma(raw_body: bytes, header: str, secret: str) -> bool: if not header: return False t, v1 = None, None for parte in header.split(","): clave, _, valor = parte.partition("=") if clave == "t": t = valor elif clave == "v1": v1 = valor if not t or not v1: return False if abs(int(time.time()) - int(t)) > 300: return False esperada = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(v1, esperada)Usa el cuerpo crudo, tal cual llegó por la red — si tu framework lo re-serializa antes de verificar, la firma no va a coincidir (detalle en la guía de webhooks).
Tras regenerar el secreto, toda llamada posterior sale firmada con el nuevo; la firma con el anterior deja de validar de inmediato.
Qué trae cada aviso
Sección titulada «Qué trae cada aviso»Todos los avisos llevan id, tipo, esEjemplo, ocurridoEn y cliente { slug, id, nombre, razonSocial, rut }.
razonSocial y rut son null mientras el cliente no los completa. Descarta los repetidos por id.
| Familia | Tipos | Campos que se agregan |
|---|---|---|
| Estado | cliente.activado, cliente.suspendido, cliente.reactivado, cliente.dado-de-baja | estadoAnterior, estadoNuevo; y motivo sólo en cliente.suspendido |
| Suscripción adicional | suscripcion-adicional. creada, cambio-de-plan, bajada-programada, bajada-aplicada, bajada-cancelada, baja-programada, baja-aplicada, baja-cancelada, paso-de-ciclo-programado, paso-de-ciclo-aplicado, paso-de-ciclo-cancelado | saas { codigo, nombre, origen }, planAnterior, planNuevo ({ codigo, nombre } o null), rigeEn |
| Cupo | cupo.umbral-80, cupo.umbral-100 | saas, metrica { codigo, nombre }, umbral, cupo, consumo, alLlegar |
En un *-cancelada, planNuevo es el plan que se canceló, no el que queda. Los avisos nunca traen dirección, datos de
personas ni quién hizo la acción.
Cuándo se dispara cada aviso de estado
Sección titulada «Cuándo se dispara cada aviso de estado»| Aviso | Cuándo |
|---|---|
cliente.activado | El cliente queda activo por primera vez: nace activo, o sale de en-preactivacion |
cliente.suspendido | El cliente pasa a suspendido desde activo o en-preactivacion, con motivo |
cliente.reactivado | Vuelve a activo un cliente que ya había estado activo antes (estaba suspendido o dado-de-baja) |
cliente.dado-de-baja | El cliente pasa a dado-de-baja desde cualquier estado |
El mismo estado escrito dos veces produce a lo más un aviso, y una suspensión por mantención que no
bloquea el API no dispara nada. motivo sólo viaja en cliente.suspendido, con uno de cuatro valores:
pago, decision-de-redcumbre, decision-del-reseller o suspension-del-reseller.
Un cuerpo de ejemplo por familia
Sección titulada «Un cuerpo de ejemplo por familia»Estado (cliente.suspendido):
{ "id": "0b6c0b3e-4f53-4cf0-9c3b-6e1f0f0f9a11", "tipo": "cliente.suspendido", "esEjemplo": false, "ocurridoEn": "2026-10-05T14:03:12.000Z", "cliente": { "slug": "panaderia-el-sol", "id": 142, "nombre": "Panadería El Sol", "razonSocial": "Panadería El Sol SpA", "rut": "76000001-9" }, "estadoAnterior": "activo", "estadoNuevo": "suspendido", "motivo": "pago"}Suscripción adicional (suscripcion-adicional.cambio-de-plan):
{ "id": "1c7d1c4f-5064-4df1-ad4c-7f2f1f1fab22", "tipo": "suscripcion-adicional.cambio-de-plan", "esEjemplo": false, "ocurridoEn": "2026-10-03T10:00:00.000Z", "cliente": { "slug": "panaderia-el-sol", "id": 142, "nombre": "Panadería El Sol", "razonSocial": "Panadería El Sol SpA", "rut": "76000001-9" }, "saas": { "codigo": "mi-saas", "nombre": "Mi SaaS", "origen": "RESELLER" }, "planAnterior": { "codigo": "mi-saas-plan-basico", "nombre": "Plan Básico" }, "planNuevo": { "codigo": "mi-saas-plan-pro", "nombre": "Plan Pro" }, "rigeEn": "2026-11-01T03:00:00.000Z"}Cupo (cupo.umbral-80):
{ "id": "2d8e2d50-6175-4e02-be5d-8030202bbc33", "tipo": "cupo.umbral-80", "esEjemplo": false, "ocurridoEn": "2026-10-02T09:30:00.000Z", "cliente": { "slug": "panaderia-el-sol", "id": 142, "nombre": "Panadería El Sol", "razonSocial": "Panadería El Sol SpA", "rut": "76000001-9" }, "saas": { "codigo": "mi-saas", "nombre": "Mi SaaS", "origen": "RESELLER" }, "metrica": { "codigo": "dte_emitidos", "nombre": "Documentos emitidos" }, "umbral": 80, "cupo": 100, "consumo": 80, "alLlegar": "seguir-cobrando"}rigeEn es null en suscripcion-adicional.creada y en los avisos *-aplicada: no hay una fecha futura
que informar, el cambio ya ocurrió. El resto de los 17 tipos sigue la misma forma por familia; el campo
exacto de cada uno está en el Swagger bajo Reseller · Avisos.
Reintentos y qué cuenta como entregado
Sección titulada «Reintentos y qué cuenta como entregado»Un aviso se entrega con hasta 12 intentos, con una espera que crece de 1 segundo a 60 segundos entre uno y el siguiente — la misma política que el resto de los webhooks salientes de la plataforma. Cada intento relee la configuración y los datos del cliente en ese momento: si desactivaste los avisos o cambiaste la dirección entre el primer intento y el siguiente, el reintento respeta lo que hay ahora, no lo que había cuando se generó el aviso.
Cuenta como entregado cualquier respuesta 2xx de tu receptor. Una redirección (3xx) no se sigue
— el aviso no se reenvía a otro destino — y cuenta como fallo. Si se agotan los 12 intentos, el aviso
queda FALLIDO con el motivo del último intento.
Un aviso queda NO_ENVIADO, sin reintentar, cuando no hay nada que enviarle: los avisos están
desactivados, el destino no se pudo armar, o el destino está bloqueado por la política de salida de la
plataforma. Mientras un intento está pendiente, el aviso aparece EN_CURSO.
Historial
Sección titulada «Historial»- Panel: Reseller → Avisos; API:
GET /{tenantSlug}/reseller/avisos/historialyGET /{tenantSlug}/reseller/avisos/historial/{avisoId}(con los intentos de entrega). - Filtros:
tipo,resultado(ENTREGADO,FALLIDO,NO_ENVIADO,EN_CURSO),clienteSlug,desde,hasta(ISO 8601), máspageylimit.
El historial se conserva 30 días desde el último intento; pasado ese plazo se purga junto con sus intentos de entrega.
Recuperar el estado de tus clientes
Sección titulada «Recuperar el estado de tus clientes»Un aviso es una notificación, no la fuente de verdad: si tu receptor estuvo caído, un aviso quedó FALLIDO o dudas
de que llegó todo, reconcilia leyendo el estado actual por la superficie de máquina con tu
credencial de reseller:
GET /{tenantSlug}/reseller/tenantslista tus clientes con su estado.GET /{tenantSlug}/reseller/tenants/{tenantId}trae la ficha de un cliente: datos, estado, plan y servicios, lo mismo que muestra tu panel.
Compara el estado leído con el último aviso que procesaste (el id te sirve para descartar repetidos) y quédate con
el estado de la ficha. Para saber qué avisos fallaron y cuándo, usa el historial.
Enviar un aviso de ejemplo
Sección titulada «Enviar un aviso de ejemplo»POST /{tenantSlug}/reseller/avisos/ejemplo con { "tipo": "cliente.suspendido" } registra un aviso de ejemplo
(esEjemplo: true, cliente de ejemplo) para probar tu receptor. Hasta 10 por hora; el exceso responde 429 con la
cabecera Retry-After. Responde 409 AVISOS_INACTIVOS si los avisos están desactivados y 409 AVISOS_SIN_SECRETO si
aún no generas el secreto.
Por API (credenciales de máquina)
Sección titulada «Por API (credenciales de máquina)»Con una credencial de reseller puedes ver la
configuración, actualizarla (activo y url), enviar ejemplos y leer el historial — exactamente lo mismo
que un RESELLER_FULL_ACCESS del panel, salvo una excepción: el secreto no se genera por API, sólo en
el panel y sólo por una persona.
| Operación | Credencial mínima |
|---|---|
Ver la configuración (GET /reseller/avisos) | lectura o escritura |
Actualizar la configuración (PUT /reseller/avisos) | escritura |
Enviar un aviso de ejemplo (POST /reseller/avisos/ejemplo) | escritura |
Listar el historial (GET /reseller/avisos/historial) | lectura o escritura |
Ver un aviso del historial (GET /reseller/avisos/historial/{avisoId}) | lectura o escritura |
| Generar el secreto | ninguna: sólo el panel, con RESELLER_FULL_ACCESS |
En modo de prueba
Sección titulada «En modo de prueba»Con una credencial de prueba, estas operaciones siguen la misma regla que el resto de la superficie de máquina del reseller: las lecturas devuelven siempre el mismo conjunto de ejemplo, y las escrituras se validan pero no cambian nada real.
-
Leer la configuración siempre devuelve:
{ "activo": true, "url": "https://api.ejemplo.test/avisos-rcapi", "secretoGenerado": true, "secretoGeneradoEn": "2026-09-01T12:00:00.000Z", "actualizadoEn": "2026-09-01T12:00:00.000Z" } -
Actualizarla responde con ese mismo conjunto, sólo reemplazando
urlyactivopor lo que enviaste en el cuerpo; no queda nada guardado. -
Enviar un aviso de ejemplo responde con un
avisoIdsimulado y no cuenta para el tope de 10 por hora. -
Leer el historial siempre devuelve las mismas cuatro filas de ejemplo — una por familia (estado, suscripción adicional, cupo) y una marcada
esEjemplo: true—, nunca tu historial real.
Como con el resto del conjunto de ejemplo, un identificador o un clienteSlug que pertenece a un cliente
real responde como inexistente: la credencial de prueba nunca llega a ver ni a tocar tus datos reales.
Consultar el cupo de un cliente
Sección titulada «Consultar el cupo de un cliente»Antes de prestar el servicio, tu sistema puede preguntarle a rCAPI cuánto cupo le queda a un cliente en uno de tus SaaS:
GET /{tenantSlug}/reseller/tenants/{tenantId}/cupo?saas=<código>
con cualquiera de tus dos credenciales. La respuesta nunca es un «cero» ambiguo: dice explícitamente, por cada métrica activa de ese SaaS, uno de tres estados:
cupo— el plan del cliente incluye una cantidad para esa métrica; traeincluidas,consumidasyquedan, ycorta: truesi tu configuración de «al llegar al cupo» escortar.sin-cupo— el plan la cobra toda como adicional, sin cantidad incluida.fuera-del-plan— el plan del cliente no tiene esa métrica: nada de lo que reportes para ella se le cobrará al cliente.
Si el cliente no tiene ninguna suscripción activa con ese SaaS, la respuesta trae suscripcion: "sin-suscripcion" y todas sus métricas en fuera-del-plan, sin consultar nada más.
{ "saas": "mesa_ayuda", "suscripcion": "activa", "alLlegarAlCupo": "cortar", "periodo": { "desde": "2026-10-01T03:00:00.000Z", "hasta": "2026-11-01T03:00:00.000Z" }, "metricas": [ { "codigo": "horas_soporte", "nombre": "Horas de soporte", "estado": "cupo", "incluidas": 10, "consumidas": 8, "quedan": 2, "corta": true } ]}Errores. 400 SAAS_DESCONOCIDO si el SaaS no existe, está archivado o no es tuyo; 400 CONSULTA_DE_CUPO_NO_APLICA si el código es de un SaaS de plataforma (ValidaFirma), que no tiene este concepto; 403 si el cliente no es tuyo.
Su propio tope, aparte del de 10.000 llamadas por hora de la credencial: 20.000 consultas por hora por SaaS y 100.000 por reseller. Superarlo responde 429 LIMITE_EXCEDIDO con Retry-After; si el tope no se puede verificar, 503 TOPE_NO_VERIFICABLE, también con Retry-After, y la consulta no se ejecuta.
En prueba, la consulta responde con los cupos del conjunto de ejemplo del reseller, sin salir a Lago ni leer datos reales.
Referencia completa
Sección titulada «Referencia completa»Cada operación, con sus campos, sus respuestas y los 17 cuerpos de webhook, está en Swagger bajo Reseller · Avisos.