Ir al contenido

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).

En tu panel, en Reseller → Avisos, con el rol de acceso completo (RESELLER_FULL_ACCESS):

  1. 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.
  2. Escribe la dirección https donde quieres recibir los avisos y activa el interruptor.
  3. 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 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/json
User-Agent: rCAPI-Webhook/1.0

La 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.

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.

FamiliaTiposCampos que se agregan
Estadocliente.activado, cliente.suspendido, cliente.reactivado, cliente.dado-de-bajaestadoAnterior, estadoNuevo; y motivo sólo en cliente.suspendido
Suscripción adicionalsuscripcion-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-canceladosaas { codigo, nombre, origen }, planAnterior, planNuevo ({ codigo, nombre } o null), rigeEn
Cupocupo.umbral-80, cupo.umbral-100saas, 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.

AvisoCuándo
cliente.activadoEl cliente queda activo por primera vez: nace activo, o sale de en-preactivacion
cliente.suspendidoEl cliente pasa a suspendido desde activo o en-preactivacion, con motivo
cliente.reactivadoVuelve a activo un cliente que ya había estado activo antes (estaba suspendido o dado-de-baja)
cliente.dado-de-bajaEl 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.

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.

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.

  • Panel: Reseller → Avisos; API: GET /{tenantSlug}/reseller/avisos/historial y GET /{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ás page y limit.

El historial se conserva 30 días desde el último intento; pasado ese plazo se purga junto con sus intentos de entrega.

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/tenants lista 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.

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.

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ónCredencial 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 secretoninguna: sólo el panel, con RESELLER_FULL_ACCESS

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 url y activo por lo que enviaste en el cuerpo; no queda nada guardado.

  • Enviar un aviso de ejemplo responde con un avisoId simulado 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.

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; trae incluidas, consumidas y quedan, y corta: true si tu configuración de «al llegar al cupo» es cortar.
  • 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.

Cada operación, con sus campos, sus respuestas y los 17 cuerpos de webhook, está en Swagger bajo Reseller · Avisos.