---
title: "Webhooks em Zig: HMAC, Idempotência e Filas sem Framework Pesado"
url: "https://ziglang.com.br/artigos/zig-webhooks-hmac-idempotencia/"
markdown_url: "https://ziglang.com.br/artigos/zig-webhooks-hmac-idempotencia.MD"
description: "Como receber webhooks em Zig com validação HMAC, limite de corpo, idempotência, resposta rápida, fila de processamento e testes de contrato para produção."
date: "2026-06-05"
author: ""
---

# Webhooks em Zig: HMAC, Idempotência e Filas sem Framework Pesado

Como receber webhooks em Zig com validação HMAC, limite de corpo, idempotência, resposta rápida, fila de processamento e testes de contrato para produção.


Webhooks parecem simples até o primeiro incidente. Um serviço externo envia um `POST`, seu servidor responde `200`, e pronto. Na prática, o endpoint precisa lidar com corpo grande demais, assinatura inválida, relógio fora de janela, repetição do mesmo evento, tentativa de replay, timeout do provedor, queda do banco, ordem de entrega diferente da esperada e deploy no meio do processamento. É o tipo de fronteira em que Zig brilha quando você quer comportamento explícito em vez de mágica de framework.

A proposta deste guia é montar uma arquitetura pragmática para **receber webhooks em Zig**: validar HMAC, limitar memória, registrar o evento de forma idempotente, responder rápido e processar depois em uma fila. O foco não é copiar o SDK de Stripe, GitHub, Lemon Squeezy ou qualquer provedor específico. O foco é o desenho que se repete em produção quando um binário Zig precisa conversar com sistemas externos sem perder eventos.

Este artigo complementa [servidor HTTP em Zig para produção](/artigos/zig-http-server-producao/), [JWT e autenticação em Zig](/artigos/zig-jwt-autenticacao-api/), [filas e workers em background](/artigos/zig-filas-workers-background/), [configuração segura com segredos e variáveis de ambiente](/artigos/zig-configuracao-segura-segredos-env/) e [OpenAPI em Zig](/artigos/zig-openapi-contratos-json-clientes/). A mentalidade é a mesma: deixar a fronteira pequena, previsível e testável.

## O problema real dos webhooks

Um webhook é uma integração assíncrona disfarçada de requisição HTTP. O provedor não quer esperar sua regra de negócio inteira. Ele quer saber se você recebeu o evento. Se sua aplicação demora, responde erro por causa de uma dependência secundária ou processa tudo dentro do handler, o provedor normalmente tenta de novo. Isso é correto, mas muda o contrato: **o mesmo evento pode chegar mais de uma vez**.

Portanto, o endpoint não deve ser escrito como uma rota comum de formulário. Ele precisa responder perguntas operacionais:

- o corpo cabe no limite definido para esse provedor?
- a assinatura HMAC bate com o segredo atual?
- o timestamp está dentro da janela aceitável?
- o `event_id` já foi recebido antes?
- o payload bruto foi persistido antes de responder?
- o processamento de negócio pode ser repetido sem duplicar efeito?
- existe trilha de auditoria para investigar divergências?

A regra prática: handler de webhook recebe, valida, grava e enfileira. Worker processa.

## Modelo de dados mínimo

Mesmo em projetos pequenos, vale separar o registro bruto do efeito de negócio. Um modelo simples funciona bem:

| Campo | Uso |
|---|---|
| `provider` | identifica a origem: `stripe`, `github`, `internal` |
| `event_id` | chave idempotente do provedor |
| `event_type` | tipo lógico: `invoice.paid`, `push`, `user.updated` |
| `received_at` | horário de chegada no seu sistema |
| `signature_ok` | resultado da validação para auditoria |
| `payload_sha256` | hash do corpo bruto |
| `payload` | corpo bruto ou referência a storage |
| `status` | `queued`, `processing`, `done`, `failed_retryable`, `failed_permanent` |
| `attempts` | quantas vezes o worker tentou processar |

A chave única deve incluir pelo menos `(provider, event_id)`. Se o provedor não envia um ID confiável, você pode usar `payload_sha256` mais timestamp e tipo, mas isso é um fallback pior. Para eventos que geram dinheiro, licença, acesso ou envio externo, idempotência por ID é indispensável.

## Handler HTTP: pequeno e defensivo

O handler não precisa entender toda a regra de negócio. Ele precisa ser chato e previsível.

```zig
const std = @import("std");

const MaxWebhookBody = 256 * 1024;

const WebhookError = error{
    MissingSignature,
    InvalidSignature,
    BodyTooLarge,
    InvalidJson,
    MissingEventId,
    StorageUnavailable,
};

pub const WebhookRequest = struct {
    provider: []const u8,
    signature: []const u8,
    timestamp: []const u8,
    body: []const u8,
};

pub fn handleWebhook(allocator: std.mem.Allocator, req: WebhookRequest) !u16 {
    if (req.body.len > MaxWebhookBody) return WebhookError.BodyTooLarge;

    try verifySignature(req.signature, req.timestamp, req.body);

    const parsed = try parseEnvelope(allocator, req.body);
    defer parsed.deinit(allocator);

    try storeAndEnqueue(.{
        .provider = req.provider,
        .event_id = parsed.event_id,
        .event_type = parsed.event_type,
        .payload = req.body,
    });

    return 202;
}
```

O status `202 Accepted` comunica melhor o contrato do que `200 OK`: você recebeu e aceitou para processamento, mas ainda não concluiu o efeito final. Alguns provedores só exigem qualquer `2xx`; outros documentam comportamento específico. Siga o provedor, mas mantenha a semântica interna.

## Validação HMAC sem inventar protocolo

A maioria dos provedores assina o corpo bruto com um segredo compartilhado. O detalhe importante é "corpo bruto". Se você parseia JSON, reserializa e assina o resultado, a assinatura muda por causa de espaços, ordem de campos e escapes. Grave e valide os bytes recebidos.

Um desenho genérico:

```zig
fn verifySignature(signature_header: []const u8, timestamp: []const u8, body: []const u8) !void {
    if (signature_header.len == 0) return WebhookError.MissingSignature;

    const secret = loadWebhookSecret();
    const signed_payload = try buildSignedPayload(timestamp, body);
    defer signed_payload.deinit();

    var mac: [32]u8 = undefined;
    std.crypto.auth.hmac.sha2.HmacSha256.create(
        &mac,
        signed_payload.items,
        secret,
    );

    const expected_hex = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "{s}",
        .{std.fmt.fmtSliceHexLower(&mac)},
    );
    defer std.heap.page_allocator.free(expected_hex);

    if (!std.crypto.timing_safe.eql([32]u8, mac, try decodeHeader(signature_header))) {
        return WebhookError.InvalidSignature;
    }
}
```

O exemplo acima é deliberadamente esquemático porque cada provedor formata a string assinada de um jeito. Alguns assinam `timestamp.body`, outros assinam apenas o body, outros mandam múltiplas assinaturas para rotação de segredo. A parte que não muda é: use HMAC da biblioteca padrão, compare em tempo constante e nunca coloque o segredo no log. Para segredos em produção, combine com o padrão de [configuração segura em Zig](/artigos/zig-configuracao-segura-segredos-env/).

## Janela de tempo e replay

HMAC prova que alguém conhece o segredo. Não prova que o evento é novo. Por isso, muitos provedores incluem timestamp no cabeçalho assinado. Rejeite eventos muito antigos, por exemplo acima de cinco minutos, se o provedor recomenda esse comportamento.

A janela reduz ataque de replay, mas não substitui idempotência. Um evento legítimo pode ser reenviado dentro da janela por timeout de rede. O endpoint deve aceitar a repetição e responder `2xx` se o evento já foi gravado. O worker não deve executar o mesmo efeito duas vezes.

## Idempotência no banco

A idempotência mais robusta não mora em memória. Mora em uma constraint do banco.

```sql
CREATE TABLE webhook_events (
  id INTEGER PRIMARY KEY,
  provider TEXT NOT NULL,
  event_id TEXT NOT NULL,
  event_type TEXT NOT NULL,
  payload_sha256 TEXT NOT NULL,
  payload TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'queued',
  attempts INTEGER NOT NULL DEFAULT 0,
  received_at TEXT NOT NULL,
  UNIQUE(provider, event_id)
);
```

No handler, tente inserir. Se a constraint falhar porque o evento já existe, responda sucesso. Não trate duplicata como erro de integração. Ela é parte normal do contrato.

```zig
const InsertResult = enum { inserted, duplicate };

fn storeAndEnqueue(event: IncomingEvent) !InsertResult {
    // Pseudocódigo: use o driver SQLite/Postgres real do projeto.
    // INSERT ... ON CONFLICT(provider, event_id) DO NOTHING
    // Se inseriu: publicar job ou deixar worker buscar status='queued'.
    // Se duplicou: não executar efeito de negócio de novo.
    return .inserted;
}
```

Para ferramentas locais e integrações pequenas, SQLite pode ser suficiente. Para múltiplas réplicas ou alto volume, Postgres tende a ser mais simples de operar com locks, retries e workers concorrentes. Veja também [SQLite em Zig para ferramentas locais](/artigos/zig-sqlite-ferramentas-locais/) e [integrações com bancos de dados em Zig](/artigos/zig-banco-dados-integracoes/).

## Responda rápido, processe depois

O erro mais comum é chamar APIs externas, enviar email, recalcular permissões e atualizar múltiplas tabelas antes de responder ao provedor. Isso transforma qualquer lentidão interna em reentrega externa.

Prefira este fluxo:

1. ler corpo com limite;
2. validar assinatura;
3. extrair `event_id` e `event_type` mínimos;
4. inserir payload bruto com chave única;
5. retornar `2xx`;
6. worker busca eventos `queued` e aplica regra de negócio.

O worker pode ter retry com backoff, dead-letter manual e métricas. O endpoint deve continuar simples. Essa separação também facilita testes: você testa validação HTTP de um lado e processamento de negócio do outro.

## Erros e códigos de resposta

Nem todo erro merece o mesmo status.

| Situação | Resposta sugerida |
|---|---|
| assinatura ausente ou inválida | `401` ou `403` |
| corpo acima do limite | `413` |
| JSON inválido | `400` |
| evento duplicado já registrado | `200` ou `202` |
| banco indisponível antes de gravar | `500` |
| falha no worker depois de gravar | handler já respondeu; worker marca retry |

Se o banco caiu antes de persistir o evento, não responda sucesso. Deixe o provedor tentar novamente. Se o evento foi persistido e o worker falhou depois, não peça reentrega HTTP; use sua fila interna.

## Testes de contrato

Webhooks quebram em detalhes pequenos: header mudou, timestamp veio em segundos em vez de milissegundos, provedor adicionou campo, assinatura passa a incluir prefixo. Guarde payloads reais sanitizados como fixtures e teste o handler contra eles.

Casos mínimos:

- payload válido com assinatura válida;
- assinatura errada;
- timestamp fora da janela;
- corpo maior que o limite;
- evento duplicado;
- JSON válido com campo extra;
- JSON sem `event_id`;
- falha de storage antes de responder.

Combine testes unitários de HMAC com teste de integração do fluxo completo. Se você usa OpenAPI para documentação interna, documente o endpoint como rota de fronteira, mas não dependa só de schema: assinatura, replay e idempotência são comportamento, não apenas formato.

## Observabilidade do endpoint

Um webhook sem observabilidade vira caixa-preta. Registre pelo menos:

- contagem por `provider` e `event_type`;
- duplicatas por provedor;
- assinaturas inválidas;
- latência do handler até persistir;
- tamanho do payload;
- idade do evento quando recebido;
- backlog e tempo de processamento do worker;
- falhas permanentes por tipo.

Não logue payload completo por padrão. Ele pode conter dados pessoais, tokens, emails, endereços ou detalhes de pagamento. Logue `event_id`, hash do payload, tipo e status. Para conectar essa rota ao resto do sistema, use as ideias de [OpenTelemetry em Zig](/artigos/zig-opentelemetry-traces-metricas-logs/) e [observabilidade em Zig](/artigos/zig-observabilidade/).

## Checklist de produção

Antes de expor o endpoint publicamente, confira:

- segredo carregado por configuração, não hardcoded;
- suporte a rotação de segredo se o provedor permitir múltiplas assinaturas;
- limite de corpo menor que o limite global do servidor;
- comparação de assinatura em tempo constante;
- janela de timestamp documentada;
- constraint única em `(provider, event_id)`;
- worker idempotente mesmo se rodar duas vezes;
- payload bruto persistido antes do `2xx`;
- métricas e logs sem vazar segredo ou payload sensível;
- fixtures de teste com payload real sanitizado;
- runbook para reprocessar evento `failed_retryable`.

## Quando Zig é uma boa escolha

Zig não torna webhook automaticamente mais seguro. O que ele oferece é controle. Você decide limite de memória, formato do erro, alocador, serialização, comparação criptográfica e caminho de persistência. Para integrações críticas, essa explicitude é vantagem: menos comportamento escondido, binário pequeno, deploy simples e testes que cobrem a fronteira real.

Se o projeto precisa só receber um formulário de baixa importância, qualquer stack resolve. Se o webhook aciona cobrança, provisionamento, licença, entrega, build, deploy ou sincronização de dados, trate a rota como infraestrutura. Em Zig, isso significa um handler pequeno, HMAC bem validado, idempotência no banco, fila clara e worker observável. O resto é detalhe de provedor.

Para comparar a ergonomia com outra linguagem muito usada em backends de integração, veja também <a href="https://golang.com.br/artigos/go-error-handling/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">tratamento explícito de erros em Go</a>. O ponto em comum é o mesmo que torna webhooks confiáveis: falhas precisam ser parte do desenho, não exceção surpresa no caminho feliz.
