Pular para o conteúdo

← Documentação

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.

Cadastrar um webhook

  1. No painel, abra Configurações → Avisos. Só proprietários e administradores da organização cadastram webhooks.
  2. 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.
  3. 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.
  4. 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 ao id do corpo, e o mesmo em toda retentativa e reenvio. Use-o para não processar o mesmo evento duas vezes.
  • Arkame-Event-Type e User-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.

TipoQuando sai
backup.failedO backup terminou em falha. Não sai quando o serviço do agente foi parado ou reiniciado no meio.
backup.partialO backup terminou incompleto: parte dos arquivos não foi salva.
backup.completeddesmarcado no cadastroO backup foi concluído. Um por backup, o dia inteiro.
anomaly.detectedMuito mais arquivos alterados ou removidos que o normal do plano. severity: warning ou critical.
anomaly.resolvedAlguém confirmou no painel que a mudança era esperada.
purge.blockedA 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.completedO teste de restauração por amostra restaurou e conferiu os arquivos de um servidor, sem falha.
verification.failedO teste de restauração achou arquivo que não restaura (critical).
inventory.completeddesmarcado no cadastroO inventário do bucket conferiu tudo, e nenhuma versão do catálogo falta.
inventory.mismatchO 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.lateA execução agendada de um plano não começou até duas horas depois do horário previsto. Um por atraso.
agent.offlineO servidor está há mais de 60 minutos sem mandar sinal. Um por episódio.
agent.onlineO servidor que gerou agent.offline voltou a responder.
restore.completedA 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.partialA restauração terminou com parte dos arquivos sem restaurar.
restore.failedA restauração terminou sem restaurar nenhum arquivo, ou a importação da conta do Zimbra falhou.
report.readyO relatório mensal da organização foi gerado.
storage.config_changedAlgué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_failedA atualização automática de um servidor falhou, e ele segue na versão anterior. Uma por tentativa.
storage.unreachableO ú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.reachableO 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 2xx em 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_at e o Arkame-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…/sendMessage do 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 →