Webhooks
Os eventos do serviço (backup que falhou, servidor sem sinal, restauração concluída, inventário do bucket) enviados por HTTPS para o seu sistema, assinados, com retentativas.
Índice
Cadastrar um webhook
- No painel, abra Configurações → Avisos. Só proprietários e administradores da organização cadastram webhooks.
- Dê um nome, escolha o formato (JSON assinado para o seu sistema; Slack, Teams ou Telegram para uma mensagem pronta) e cole a URL de destino. Ela precisa ser
https://, num endereço público na internet. - Marque os eventos. Confirme com o código da verificação em duas etapas: um webhook envia nomes de servidores e de planos para fora da Arkame.
- No formato JSON, copie o segredo de assinatura (
whsec_…): ele aparece uma vez só. Depois, use Enviar teste para conferir o destino.
O pedido
Cada evento é um POST com Content-Type: application/json. O que varia por tipo fica em 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: o carimbo de tempo e a assinatura do corpo (veja abaixo).Arkame-Event-Id: igual aoiddo corpo, e o mesmo em toda retentativa e reenvio. Use-o para não processar o mesmo evento duas vezes.Arkame-Event-TypeeUser-Agent: Arkame-Webhooks/1.
Dentro de api_version: 2026-10-01, só há mudança aditiva: um campo novo pode aparecer em details; nenhum campo existente muda de nome, de tipo ou de sentido.
Tipos de evento
Você escolhe quais receber em cada webhook, e pode mudar a escolha depois. Um tipo novo não chega sozinho a um webhook que já existia: marque-o na tela.
| Tipo | Quando sai |
|---|---|
backup.failed | O backup terminou em falha. Não sai quando o serviço do agente foi parado ou reiniciado no meio. |
backup.partial | O backup terminou incompleto: parte dos arquivos não foi salva. |
backup.completeddesmarcado no cadastro | O backup foi concluído. Um por backup, o dia inteiro. |
anomaly.detected | Muito mais arquivos alterados ou removidos que o normal do plano. severity: warning ou critical. |
anomaly.resolved | Alguém confirmou no painel que a mudança era esperada. |
purge.blocked | A limpeza de versões antigas de um armazenamento foi suspensa, por mudança brusca num backup ou por versão que não restaura. |
verification.completed | O teste de restauração por amostra restaurou e conferiu os arquivos de um servidor, sem falha. |
verification.failed | O teste de restauração achou arquivo que não restaura (critical). |
inventory.completeddesmarcado no cadastro | O inventário do bucket conferiu tudo, e nenhuma versão do catálogo falta. |
inventory.mismatch | O inventário achou diferença entre o catálogo e o bucket: critical quando falta versão; warning no que não impede restaurar. |
backup.late | A execução agendada de um plano não começou até duas horas depois do horário previsto. Um por atraso. |
agent.offline | O servidor está há mais de 60 minutos sem mandar sinal. Um por episódio. |
agent.online | O servidor que gerou agent.offline voltou a responder. |
restore.completed | A restauração terminou com todos os arquivos restaurados. Na conta do Zimbra, sai quando a conta foi importada no Zimbra, e não quando os arquivos chegam ao servidor. |
restore.partial | A restauração terminou com parte dos arquivos sem restaurar. |
restore.failed | A restauração terminou sem restaurar nenhum arquivo, ou a importação da conta do Zimbra falhou. |
report.ready | O relatório mensal da organização foi gerado. |
storage.config_changed | Alguém mudou um armazenamento ou a política de retenção dele, ou uma mudança agendada entrou em vigor. details.changed diz quais campos, nunca os valores; warning quando muda para onde o backup vai ou encurta a proteção (retenção menor, exclusão definitiva, desativar). |
agent.update_failed | A atualização automática de um servidor falhou, e ele segue na versão anterior. Uma por tentativa. |
storage.unreachable | O último teste do bucket de um armazenamento falhou e nenhum servidor em uso o alcança: o bucket foi apagado, o cadastro mudou, ou a chave e a rede falham em todos. Um por episódio, critical. details.cause é bucket, chave, rede ou outra; o erro cru do provedor fica só no painel. |
storage.reachable | O bucket que gerou storage.unreachable voltou a responder a um servidor. |
Há também o ping, que só sai pelo botão Enviar teste.
Conferir a assinatura
Arkame-Signature: t=SEGUNDOS,v1=HEX, em que v1 é o HMAC-SHA256 de t + "." + corpo, com o segredo inteiro (whsec_…) como chave. Calcule sobre o corpo cru, exatamente como chegou, e recuse o pedido com mais de 5 minutos: sem isso, um pedido capturado valeria para sempre.
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']);
}Guarde o segredo como uma senha. Se ele vazar, remova o webhook e cadastre outro: o segredo novo aparece no cadastro.
Entrega e retentativas
- Sucesso é qualquer resposta
2xxem até 5 segundos: responda primeiro e processe depois. Redirecionamento (3xx) conta como falha. - Em falha, tentamos de novo depois de 1 min, 5 min, 30 min, 2 h, 6 h e 24 h. A ordem de chegada pode variar entre eventos: use
created_ate oArkame-Event-Id. - 20 falhas seguidas, ou 3 dias falhando, desativam o webhook, e os proprietários e administradores recebem um e-mail. Reativar é na mesma tela.
- A tela mostra as últimas entregas e o que o destino respondeu. A entrega que terminou pode ser reenviada: sai de novo com o mesmo id de evento.
- Só
https, nas portas 443 ou 8443, num endereço público. Endereços de rede interna são recusados, inclusive um nome público que aponte para um deles. Um n8n dentro da sua rede precisa de um endereço público na frente.
Slack, Teams e Telegram
O mesmo evento, já como mensagem, no idioma escolhido no cadastro:
- Slack: a URL de um Incoming Webhook do canal.
- Teams: a URL de um fluxo “Quando uma solicitação de webhook do Teams for recebida” (Workflows).
- Telegram: a URL
https://api.telegram.org/bot123456:ABC…/sendMessagedo seu bot e o chat (id numérico ou@canal). A URL fica guardada cifrada.
n8n, Make e Zapier: use o formato JSON com o gatilho de webhook de cada um, e confira a assinatura num passo de código.
Pronto para cadastrar?
Os webhooks ficam em Configurações → Avisos, junto dos endereços de e-mail que recebem os avisos.
Abrir os avisos no painel →