Skip to main content
Todo webhook da Chargefy é entregue de forma assíncrona, assinado com HMAC-SHA-256 e reentregue automaticamente em caso de falha. A assinatura segue o padrão Standard Webhooks por completo — inclusive o formato do secret — então você verifica com qualquer biblioteca compatível, sem reimplementar HMAC manual.
O que você precisa garantir no seu endpoint
  1. Aceitar POST em uma URL HTTPS.
  2. Verificar a assinatura antes de processar o corpo.
  3. Responder com um status 2xx em até 20 segundos.
O resto — fila, retry, rotação de secret, fan-out — é responsabilidade da Chargefy.

Como a entrega funciona

A entrega é desacoplada do evento. Quando algo acontece (um pagamento confirma, uma assinatura é criada), a Chargefy grava o evento e enfileira a entrega; um worker dedicado drena a fila e faz o POST. Isso garante que um endpoint lento ou fora do ar nunca trava a operação que originou o evento.
EtapaO que acontece
1. EventoUma mudança em recurso público gera um evento com id (evt_...) e payload congelado naquele instante.
2. EnfileiramentoO evento é gravado e enfileirado uma vez por endpoint inscrito naquele tipo.
3. EntregaUm worker drena a fila a cada minuto, monta os headers assinados e faz o POST para a URL do endpoint.
4. ResultadoResposta 2xx marca a entrega como concluída. Qualquer outra coisa agenda uma reentrega.
Cada endpoint inscrito recebe seu próprio evento, com id próprio. Se você tem dois endpoints ouvindo payment_intent.succeeded, o mesmo fato gera dois eventos independentes — cada um com seu ciclo de entrega e retry.

Parâmetros de entrega

ParâmetroValorOrigem
Timeout por tentativa20 segundosConexão abortada após esse limite.
Critério de sucessostatus 2xx (200299)3xx, 4xx, 5xx e timeout contam como falha.
Máximo de tentativas10Inclui a primeira tentativa.
Cadência de reentrega~1 minuto entre tentativasO worker roda a cada minuto; um evento que falhou volta a ser elegível no próximo ciclo.
Janela total de retry~10 minutosDepois disso a entrega vira falha permanente.
Método HTTPPOSTSempre.
Content-Typeapplication/jsonCorpo é o JSON do evento.
A reentrega usa cadência fixa de aproximadamente um minuto, não backoff exponencial de horas. Se o seu endpoint ficar fora do ar por mais de ~10 minutos, o evento atinge o limite de tentativas e é marcado como falha permanente — recupere-o por reentrega manual ou pela API depois que o endpoint voltar.

Retenção do payload

O payload do evento é mantido por 30 dias. Depois disso ele é esvaziado automaticamente (archival), mas os metadados da entrega (status, data, HTTP code, corpo de resposta truncado) permanecem para auditoria.
Reentrega manual só é possível enquanto o payload existe. Após o archival de 30 dias não há corpo para reenviar — a tentativa retorna erro indicando que o payload foi arquivado.

O envelope do evento

O corpo entregue é sempre o envelope canônico de evento. O recurso afetado vive em data.object, no mesmo shape público retornado pela API. As chaves de todo objeto JSON público seguem a ordem canônica: id, object, depois alfabética.
{
  "id": "evt_123",
  "object": "event",
  "created_at": "2026-05-16T14:09:27+00:00",
  "data": {
    "object": {
      "id": "pi_123",
      "object": "payment_intent",
      "amount": 9900,
      "currency": "brl",
      "status": "succeeded"
    }
  },
  "livemode": true,
  "organization": "org_123",
  "request": {
    "id": "req_123"
  },
  "type": "payment.intent.succeeded"
}
CampoTipoDescrição
idstringID único do evento. É idêntico ao header webhook-id — use-o para idempotência.
objectstringSempre "event".
created_atstringISO 8601 do momento em que o evento foi criado.
data.objectobjectObjeto público completo no estado atual. Mesmo DTO do GET single.
data.previous_attributesobjectSó em eventos de atualização. Contém apenas os campos alterados, com o valor anterior. Ausente em eventos de criação.
livemodebooleantrue em produção, false em teste.
organizationstringOrganização que originou o evento. Veja fan-out de plataforma.
request.idstringRequest (req_*) que originou o evento, quando disponível.
typestringTipo do evento (<recurso>.<ação>). Catálogo em Eventos.
data.previous_attributes traz o diff. Para customer.updated onde só o e-mail mudou, você recebe { "email": "antigo@email.com" } — o valor novo já está em data.object. A resposta direta da API de update não carrega diff; o diff vive apenas no webhook.

Assinatura

Cada requisição carrega os headers abaixo. A assinatura cobre o id, o timestamp e o corpo bruto — qualquer re-serialização do corpo invalida a assinatura.
HeaderDescriçãoExemplo
webhook-idID único do evento. Igual ao id do corpo.evt_123
webhook-timestampUnix timestamp em segundos de quando a entrega foi assinada.1747405767
webhook-signatureUma ou mais assinaturas, separadas por espaço. Cada uma no formato v1,<base64>.v1,K7gNU3sdo+OL0wNh...
content-typeSempre application/json.application/json
user-agentIdentifica o emissor.chargefy.io webhooks

Formato do secret

Cada endpoint tem um secret no formato canônico:
whsec_<base64 de 32 bytes aleatórios>
Os bytes decodificados do base64 (após o prefixo whsec_) são a chave do HMAC. Esse é exatamente o ponto que garante compatibilidade com as bibliotecas Standard Webhooks: você passa o secret inteiro, com prefixo, e a lib cuida da decodificação.
Secrets no formato antigo chargefy_whs_... foram descontinuados. Endpoints que ainda carregam um secret legado falham na assinatura — a entrega é registrada como erro pedindo a rotação. Gere um novo secret em Configurações → Webhooks → Resetar secret.

Algoritmo de assinatura

A string assinada é a concatenação de três partes separadas por ponto:
assinatura = HMAC_SHA256(
  chave = base64_decode(secret_sem_prefixo_whsec_),
  dados = `${webhook-id}.${webhook-timestamp}.${corpo_bruto}`
)
header  = "v1," + base64(assinatura)
ComponenteValor
AlgoritmoHMAC com SHA-256.
ChaveBytes decodificados do base64 do secret (não a string crua).
Mensagem${webhook-id}.${webhook-timestamp}.${raw_body} — IDs e timestamp como strings, corpo exatamente como recebido.
Saída no headerv1, + assinatura em base64.

Verificação

A forma mais simples e segura é usar uma biblioteca compatível com Standard Webhooks: ela decodifica o secret, valida a janela de timestamp, faz comparação constant-time e suporta múltiplas assinaturas durante rotação. Existe implementação oficial em Node, Python, Ruby, Go, PHP, Java, C# e Rust.
import { Webhook } from 'svix';
import express from 'express';

const app = express();
const wh = new Webhook(process.env.CHARGEFY_WEBHOOK_SECRET!); // whsec_...

// IMPORTANTE: use raw body — qualquer re-serialização quebra a assinatura.
app.post('/webhooks/chargefy', express.raw({ type: 'application/json' }), (req, res) => {
  let evt: {
    id: string;
    object: 'event';
    type: string;
    organization: string;
    created_at: string;
    livemode: boolean;
    data: { object: unknown; previous_attributes?: Record<string, unknown> };
  };
  try {
    evt = wh.verify(req.body, {
      'webhook-id': req.headers['webhook-id'] as string,
      'webhook-timestamp': req.headers['webhook-timestamp'] as string,
      'webhook-signature': req.headers['webhook-signature'] as string,
    }) as typeof evt;
  } catch {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  switch (evt.type) {
    case 'payment_intent.succeeded':
      handlePaymentIntentSucceeded(evt.data.object);
      break;
    case 'customer.updated':
      handleCustomerUpdated(evt.data.object, evt.data.previous_attributes);
      break;
  }

  res.status(200).json({ received: true });
});
Sempre receba o corpo bruto (express.raw(), req.text(), request.get_data()). Se o corpo for parseado como JSON antes da verificação, a serialização pode diferir do que foi assinado e a assinatura não vai bater.

Verificação manual

Se preferir não usar uma biblioteca, o algoritmo é direto:
1

Valide a janela de tempo

Rejeite se webhook-timestamp estiver fora de ±5 minutos do horário atual. Isso bloqueia replay de requisições antigas capturadas.
2

Monte a string assinada

signed = ${webhook-id}.${webhook-timestamp}.${raw_body}, usando o corpo bruto exatamente como recebido.
3

Decodifique o secret

Remova o prefixo whsec_ e decodifique o restante de base64. Esses bytes são a chave do HMAC.
4

Calcule e compare

esperado = base64(HMAC_SHA256(chave, signed)). Compare (constant-time) contra cada assinatura no header webhook-signature, ignorando o prefixo v1,. Aceite se qualquer uma bater.
Verificação manual (TypeScript / Web Crypto)
async function verify(rawBody: string, headers: Headers, secret: string) {
  const id = headers.get('webhook-id')!;
  const ts = headers.get('webhook-timestamp')!;
  const sigHeader = headers.get('webhook-signature')!;

  // 1. Janela de ±5 minutos
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  // 2. String assinada
  const signed = `${id}.${ts}.${rawBody}`;

  // 3. Secret → bytes da chave
  const bin = atob(secret.replace(/^whsec_/, ''));
  const keyBytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
  const key = await crypto.subtle.importKey(
    'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );

  // 4. HMAC → base64 → comparação contra cada assinatura
  const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signed));
  const expected = btoa(String.fromCharCode(...new Uint8Array(mac)));
  return sigHeader
    .split(' ')
    .map((part) => part.replace(/^v1,/, ''))
    .some((candidate) => candidate === expected); // use comparação constant-time em produção
}

Rotação de secret e múltiplas assinaturas

O header webhook-signature pode conter mais de uma assinatura, separadas por espaço:
webhook-signature: v1,assinatura_secret_antigo v1,assinatura_secret_novo
Isso permite rotacionar o secret sem downtime: durante a janela de transição, a entrega é assinada com ambos os secrets, e seu endpoint aceita o evento se qualquer uma das assinaturas for válida. As bibliotecas compatíveis já fazem essa varredura automaticamente; na verificação manual, itere sobre todas as assinaturas do header.
1

Gere o novo secret

Em Configurações → Webhooks, use Resetar secret. O endpoint passa a assinar com o novo secret.
2

Atualize seu servidor

Coloque o novo whsec_... na configuração do seu endpoint e faça o deploy.
3

Confirme as entregas

Verifique no painel de entregas que os eventos voltaram a chegar com 2xx usando o novo secret.

Idempotência

Reentregas e reenvios manuais significam que o mesmo evento pode chegar mais de uma vez. Trate a entrega como at-least-once: garanta que processar o mesmo evento duas vezes não duplica efeitos no seu sistema. O campo id do envelope (igual ao header webhook-id) é estável por evento. Use-o como chave de deduplicação:
1

Persista o id do evento

Ao receber, tente gravar evt_... em uma tabela com restrição de unicidade.
2

Pule duplicatas

Se o id já existe, responda 200 e não reprocesse — a Chargefy já considera a entrega concluída.
3

Confie no estado, não na ordem

A ordem de entrega entre eventos diferentes não é garantida. Use sempre data.object como verdade do estado atual, em vez de assumir sequência.

Fan-out para plataformas

Quando uma organização conectada a uma plataforma origina um evento, a entrega vai para:
  1. Os endpoints da própria organização que originou o evento (se ela tiver algum inscrito).
  2. Os endpoints de cada plataforma ativa que tem essa organização como conectada.
O payload é idêntico nos dois casos — só muda o endpoint destinatário. Em ambos, o campo top-level organization aponta para a organização que originou o evento (a organização conectada filha), nunca para a organização dona da plataforma.
Isso permite que a plataforma identifique de qual organização conectada o evento veio lendo organization, sem precisar de nenhum campo extra de relacionamento. Cada fan-out gera um evento com id próprio, então a idempotência continua valendo por destinatário.

Reentrega manual

Se um endpoint ficou fora do ar e o evento esgotou as tentativas automáticas — ou se você só quer reprocessar um evento — é possível reenviar manualmente pelo dashboard, em Configurações → Webhooks → Entregas.
CaracterísticaComportamento
DisponibilidadeEnquanto o payload existir (até 30 dias após o evento).
Eventos elegíveisTanto os que falharam quanto os que já tiveram sucesso.
Endpoint removidoNão é possível reenviar — não há destino.
HistóricoUm reenvio que falha não apaga um sucesso anterior; cada tentativa registra sua própria entrega.
Para validar seu endpoint contra o shape mais recente de um recurso sem esperar atividade real, use o reenvio — o payload é reconstruído a partir do estado atual no momento do reenvio.

Desenvolvimento local

Para testar webhooks na sua máquina, exponha a porta local com um tunnel HTTPS e cadastre a URL gerada como endpoint:
npm install -g ngrok
ngrok http 3000
# Use a URL https://<sub>.ngrok.io/webhooks/chargefy como endpoint

Boas práticas

Verifique a assinatura, persista o evento e responda 200 imediatamente. Processe a lógica pesada em background. Acima de 20 segundos a entrega é considerada falha e reentregue.
Nunca processe um webhook sem validar a assinatura HMAC. Sem isso, qualquer um que conheça sua URL pode forjar eventos.
Grave o id (evt_...) e ignore repetições. Reentregas e reenvios manuais tornam a entrega at-least-once.
URLs de webhook devem usar HTTPS. Endpoints HTTP não são aceitos.
Acompanhe o status em Configurações → Webhooks → Entregas e investigue falhas persistentes antes que esgotem as tentativas.

Solução de problemas

SintomaCausa provávelO que fazer
Você responde 401/erro de assinaturaSecret errado, corpo já parseado, ou secret legado chargefy_whs_Use o secret whsec_... correto, receba o corpo bruto e rotacione secrets antigos.
Eventos param de chegarURL inválida, não-HTTPS, firewall, ou endpoint removidoConfira a URL (HTTPS) e o status do endpoint no dashboard.
Tudo chega como timeoutHandler lento (> 20s)Responda 200 na hora e processe em background.
Evento recebido mais de uma vezReentrega após falha ou reenvio manualDedupe pelo id do evento.
Falha permanente após ~10 minEndpoint indisponível além da janela de retryResolva o endpoint e use a reentrega manual.

Próximos passos

Eventos disponíveis

Catálogo completo de eventos e o shape de cada data.object.

API Reference

Recursos públicos, autenticação e convenções da API.