Webhooks
Los eventos del servicio (una copia que falló, un servidor sin señal, una restauración terminada, el inventario del bucket) enviados por HTTPS a tu sistema, firmados y con reintentos.
Índice
Registrar un webhook
- En el panel, abre Configuración → Avisos. Solo los propietarios y administradores de la organización registran webhooks.
- Ponle un nombre, elige el formato (JSON firmado para tu sistema; Slack, Teams o Telegram para un mensaje listo) y pega la URL de destino. Tiene que ser
https://, en una dirección pública de internet. - Marca los eventos. Confirma con el código de la verificación en dos pasos: un webhook envía nombres de servidores y de planes fuera de Arkame.
- En el formato JSON, copia el secreto de firma (
whsec_…): aparece una sola vez. Después, usa Enviar prueba para comprobar el destino.
La solicitud
Cada evento es un POST con Content-Type: application/json. Lo que varía por tipo está en data.details:
{
"id": "0b6f0c1e-5f1a-4f6e-9d1e-2a7c3e9b8d41",
"type": "backup.failed",
"api_version": "2026-10-01",
"created_at": "2026-10-13T12:00:00.000Z",
"organization": { "id": "…", "name": "Acme" },
"data": {
"severity": "warning",
"subject": { "type": "session", "id": "…" },
"details": {
"error_code": "storage_unreachable",
"agent_id": "…", "agent_name": "fs-01",
"plan_id": "…", "plan_name": "Nightly",
"storage_id": "…", "storage_name": "Main bucket",
"panel_url": "https://save.arkame.app/historico/backups/…"
}
}
}Arkame-Signature: la marca de tiempo y la firma del cuerpo (ver abajo).Arkame-Event-Id: igual aliddel cuerpo, y el mismo en cada reintento y reenvío. Úsalo para no procesar el mismo evento dos veces.Arkame-Event-TypeyUser-Agent: Arkame-Webhooks/1.
Dentro de api_version: 2026-10-01, solo hay cambios aditivos: puede aparecer un campo nuevo en details; ningún campo existente cambia de nombre, de tipo ni de sentido.
Tipos de evento
Eliges cuáles recibe cada webhook, y puedes cambiarlo después. Un tipo nuevo no llega solo a un webhook que ya existía: márcalo en la pantalla.
| Tipo | Cuándo se envía |
|---|---|
backup.failed | La copia terminó con error. No se envía cuando el servicio del agente se detuvo o se reinició en medio de la copia. |
backup.partial | La copia terminó incompleta: parte de los archivos no se guardó. |
backup.completeddesmarcado al registrar | La copia terminó. Uno por copia, todo el día. |
anomaly.detected | Muchos más archivos cambiados o borrados que lo normal del plan. severity: warning o critical. |
anomaly.resolved | Alguien confirmó en el panel que el cambio era esperado. |
purge.blocked | La limpieza de versiones antiguas de un almacenamiento quedó suspendida, por un cambio brusco en una copia o por una versión que no se restaura. |
verification.completed | La prueba de restauración por muestra restauró y comprobó los archivos de un servidor, sin errores. |
verification.failed | La prueba de restauración encontró un archivo que no se restaura (critical). |
inventory.completeddesmarcado al registrar | El inventario del bucket comprobó todo, y no falta ninguna versión del catálogo. |
inventory.mismatch | El inventario encontró una diferencia entre el catálogo y el bucket: critical cuando falta una versión; warning en lo que no impide restaurar. |
backup.late | La ejecución programada de un plan no empezó dos horas después de la hora prevista. Uno por atraso. |
agent.offline | El servidor lleva más de 60 minutos sin dar señal. Uno por episodio. |
agent.online | El servidor que generó agent.offline volvió a responder. |
restore.completed | La restauración terminó con todos los archivos restaurados. En la cuenta de Zimbra, se envía cuando la cuenta ya fue importada en Zimbra, no cuando los archivos llegan al servidor. |
restore.partial | La restauración terminó con parte de los archivos sin restaurar. |
restore.failed | La restauración terminó sin restaurar ningún archivo, o la importación de la cuenta de Zimbra falló. |
report.ready | Se generó el informe mensual de la organización. |
storage.config_changed | Alguien cambió un almacenamiento o su política de retención, o entró en vigor un cambio programado. details.changed dice qué campos, nunca los valores; warning cuando cambia adónde va la copia o acorta la protección (retención menor, borrado definitivo, desactivar). |
agent.update_failed | La actualización automática de un servidor falló, y sigue en la versión anterior. Una por intento. |
storage.unreachable | La última prueba del bucket de un almacenamiento falló y ningún servidor en uso lo alcanza: el bucket fue borrado, el registro cambió, o la clave y la red fallan en todos. Uno por episodio, critical. details.cause es bucket, chave (clave), rede (red) u outra (otra); el error en bruto del proveedor queda solo en el panel. |
storage.reachable | El bucket que generó storage.unreachable volvió a responder a un servidor. |
También existe el ping, que solo se envía con el botón Enviar prueba.
Comprobar la firma
Arkame-Signature: t=SEGUNDOS,v1=HEX, donde v1 es el HMAC-SHA256 de t + "." + cuerpo, con el secreto entero (whsec_…) como clave. Calcúlalo sobre el cuerpo crudo, tal como llegó, y rechaza la solicitud con más de 5 minutos: sin eso, una solicitud capturada valdría para siempre.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the text as received, before any JSON.parse.
export function isValidSignature(secret, header, rawBody) {
const p = Object.fromEntries(header.split(',').map((x) => x.trim().split('=')));
if (!/^\d+$/.test(p.t ?? '') || !p.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest();
const received = Buffer.from(p.v1, 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}Python
import hashlib, hmac, time
# raw_body: the bytes as received, before any json.loads.
def is_valid_signature(secret: str, header: str, raw_body: bytes) -> bool:
try:
p = dict(x.strip().split("=", 1) for x in header.split(","))
t = int(p["t"])
except (KeyError, ValueError):
return False
if "v1" not in p or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), p["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, p["v1"])PHP
// $rawBody: file_get_contents('php://input'), before json_decode.
function is_valid_signature(string $secret, string $header, string $rawBody): bool {
parse_str(str_replace(',', '&', $header), $p);
if (empty($p['t']) || empty($p['v1']) || abs(time() - (int) $p['t']) > 300) return false;
return hash_equals(hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret), $p['v1']);
}Guarda el secreto como una contraseña. Si se filtra, elimina el webhook y registra otro: el secreto nuevo aparece al registrarlo.
Entrega y reintentos
- Éxito es cualquier respuesta
2xxen hasta 5 segundos: responde primero y procesa después. Una redirección (3xx) cuenta como error. - Si falla, volvemos a intentar después de 1 min, 5 min, 30 min, 2 h, 6 h y 24 h. El orden de llegada puede variar entre eventos: usa
created_aty elArkame-Event-Id. - 20 errores seguidos, o 3 días fallando, desactivan el webhook, y los propietarios y administradores reciben un correo. Se reactiva en la misma pantalla.
- La pantalla muestra las últimas entregas y lo que respondió el destino. Una entrega terminada se puede reenviar: sale de nuevo con el mismo id de evento.
- Solo
https, en los puertos 443 u 8443, en una dirección pública. Se rechazan las direcciones de red interna, también un nombre público que apunte a una de ellas. Un n8n dentro de tu red necesita una dirección pública delante.
Slack, Teams y Telegram
El mismo evento, ya como mensaje, en el idioma elegido al registrarlo:
- Slack: la URL de un Incoming Webhook del canal.
- Teams: la URL de un flujo “Cuando se recibe una solicitud de webhook de Teams” (Workflows).
- Telegram: la URL
https://api.telegram.org/bot123456:ABC…/sendMessagede tu bot y el chat (id numérico o@canal). La URL se guarda cifrada.
n8n, Make y Zapier: usa el formato JSON con el disparador de webhook de cada uno, y comprueba la firma en un paso de código.
¿Listo para registrar uno?
Los webhooks están en Configuración → Avisos, junto a los correos que reciben los avisos.
Abrir los avisos en el panel →