Webhooks
A Wolf avisa o seu servidor sobre pagamentos, pedidos, reembolsos e mais, com um POST HTTPS assinado por evento. Verifique a assinatura, responda 2xx rápido e trate cada event.id uma única vez.
Endpoints
- Cadastre uma URL HTTPS pública no painel (Webhooks) ou por
POST /v1/webhooks/endpoints. URLs da própria Wolf e endereços privados são recusados. - Modo
live: entregas reais e assinadas. Criar um endpointliveexige sessão humana do painel, porque revela o signing secret (whsec_live_...) uma única vez. - Modo
simulated: nenhuma requisição HTTP, só o registro no painel. Não existe em produção. selected_eventsvazio recebe todos os eventos; com itens, só os tipos listados. O eventowebhook.testignora o filtro.
Operações
- get
/v1/webhooks/deliveries— Listar entregas recentes - post
/v1/webhooks/deliveries/{id}/replay— Reenviar entrega (replay) - get
/v1/webhooks/endpoints— Listar endpoints - post
/v1/webhooks/endpoints— Criar endpoint - patch
/v1/webhooks/endpoints/{id}— Atualizar endpoint (status e filtros) - delete
/v1/webhooks/endpoints/{id}— Excluir endpoint - post
/v1/webhooks/endpoints/{id}/rotate-secret— Rotacionar signing secret - post
/v1/webhooks/endpoints/{id}/test— Enviar evento de teste
Eventos
O corpo é o envelope do evento. data nunca traz dados do pagador (nome, e-mail ou documento).
{
"id": "evt_7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e",
"type": "payment.paid",
"version": 1,
"seller_id": "sel_...",
"resource_id": "pi_3b9f0c2d1e4a5b6c7d8e9f0a1b2c3d4e",
"created_at": "2026-11-06T12:00:05.000Z",
"data": {
"payment_intent_id": "pi_3b9f0c2d1e4a5b6c7d8e9f0a1b2c3d4e",
"order_id": null,
"status": "succeeded",
"amount_minor": "9900",
"currency": "BRL",
"provider_id": "simulator",
"payment_method_type": "pix"
}
}| Tipo | Versão | Quando |
|---|---|---|
affiliate.commission.created | 1 | Reservado; afiliados estão fora desta edição. |
checkout.session.completed | 1 | Sessão de checkout concluída após o pagamento. |
checkout.session.created | 1 | Reservado; não é emitido nesta edição. |
dispute.created | 1 | Disputa sintética aberta pela operação da Wolf. |
dispute.lost | 1 | Disputa sintética perdida pelo seller. |
dispute.won | 1 | Disputa sintética ganha pelo seller. |
order.created | 1 | Pedido criado (pela API ou pelo checkout hospedado), com os itens (oferta principal e order bumps). |
order.paid | 1 | Pedido pago — acontece exatamente uma vez; traz os itens do pedido. |
payment.authorized | 1 | Reservado; não é emitido nesta edição (não há autorização separada da captura). |
payment.canceled | 1 | PaymentIntent cancelado; Pix vencido traz cancellation_reason: "expired". |
payment.created | 1 | PaymentIntent criado. |
payment.failed | 1 | Pagamento recusado na confirmação. |
payment.paid | 1 | Pagamento concluído (succeeded) na confirmação ou pelo pagamento do Pix: Charge criada e ledger lançado. |
payment.processing | 1 | Pagamento em processamento. Para Pix, o QR existe e data.expires_at informa o vencimento. |
payment.reconciled | 1 | A reconciliação resolveu um pagamento de resultado desconhecido; data.status traz o estado final (succeeded ou failed). |
payment.reconciliation_required | 1 | O resultado no provedor é desconhecido; a reconciliação automática vai resolver. |
payout.created | 1 | Repasse sintético criado pela operação da Wolf (data.origin: "admin"). |
payout.failed | 1 | Repasse ou saque sintético falhou e o valor voltou ao saldo disponível; data.origin indica quem pediu. |
payout.paid | 1 | Repasse ou saque sintético pago; data.origin indica quem pediu (seller ou admin). |
payout.requested | 1 | Saque sintético pedido pelo seller no app (data.origin: "seller"); resolvido de forma sintética em cerca de 2 minutos. |
reconciliation.case_opened | 1 | Divergência de reconciliação aberta para um recurso do seller. |
reconciliation.case_resolved | 1 | Divergência de reconciliação resolvida. |
refund.completed | 1 | Reembolso concluído. |
refund.created | 1 | Reembolso criado. |
refund.failed | 1 | Reembolso recusado. |
refund.reconciled | 1 | A reconciliação resolveu o reembolso. |
refund.reconciliation_required | 1 | Resultado do reembolso desconhecido; em reconciliação. |
seller.created | 1 | Reservado; não é emitido nesta edição. |
seller.reactivated | 1 | A conta do seller foi reativada. |
seller.suspended | 1 | A conta do seller foi suspensa pela operação da Wolf. |
settlement.completed | 1 | Lote de liquidação sintético concluído. |
settlement.created | 1 | Lote de liquidação sintético criado. |
settlement.requires_review | 1 | Lote de liquidação sintético exige revisão da operação. |
webhook.test | 1 | Evento de teste enviado sob demanda (POST /v1/webhooks/endpoints/{id}/test); ignora selected_events. |
A ordem de entrega não é garantida e o mesmo evento pode chegar mais de uma vez (retries, replays). Use id para deduplicar e consulte o recurso na API quando precisar do estado atual.
Ciclo de vida do endpoint
| Estado | Significado | Pode ir para |
|---|---|---|
active | Recebe entregas. | disabled,deleted |
disabled | Não recebe nada. disabled_reason é manual (você desativou) ou consecutive_failures (a Wolf desativou após 100 falhas seguidas do seu receptor). | active,deleted |
deletedfinal | Excluído (final): some da listagem e responde 404. | — |
- Desativar e reativar:
PATCH /v1/webhooks/endpoints/{id}com{"status": "disabled"}ou{"status": "active"}. Reativar zera o contador de falhas. - Excluir:
DELETE /v1/webhooks/endpoints/{id}(não tem volta). - Auto-desativação: após 100 falhas consecutivas atribuíveis ao seu receptor o endpoint vira
disablede o painel mostra um aviso. Falhas da própria Wolf não contam. - Testar (
/test) e reenviar (/replay) exigem endpointactive(senão409 WEBHOOK_ENDPOINT_DISABLED).
2xx em até 10 segundos. Enfileire o processamento e responda antes; o receptor do Quickstart mostra o padrão.Verificando a assinatura dos webhooks
Toda entrega live da Wolf é assinada com HMAC-SHA256 usando o signing secret do endpoint (whsec_live_...). Verifique a assinatura antes de processar o evento e rejeite (400) o que não passar. Este guia descreve o formato real das entregas (o mesmo código que assina os eventos em produção) e é publicado, a partir deste arquivo, na página Webhooks do portal de desenvolvedores.
Endpointssimulatednão recebem requisições HTTP: a entrega é só registrada no painel. Produção entrega apenas em modolive.
A requisição
POST para a URL HTTPS cadastrada, HTTP/1.1, corpo JSON em UTF-8. Nomes de header não diferenciam maiúsculas de minúsculas; a Wolf os envia em minúsculas.
| Header | Exemplo | Significado |
|---|---|---|
content-type | application/json | Sempre JSON. |
user-agent | WolfPayment-Webhooks/2 | Identifica o remetente (não é prova de autenticidade). |
x-wolf-event-id | evt_... | Id estável do evento: o mesmo em retries, replays e testes reenviados. Use-o para deduplicar. |
x-wolf-event-type | payment.paid, webhook.test | Tipo do evento (igual a type no corpo). |
x-wolf-timestamp | 1790510400 | Segundos Unix (UTC) do momento da assinatura desta tentativa. |
x-wolf-signature | v1=ebf4b0f7...4c37 | v1= seguido do HMAC-SHA256 em hexadecimal minúsculo (64 caracteres). |
wolf-delivery-attempt | 1 | Número da tentativa para este endpoint e evento: 1 na primeira; retries automáticos e replays incrementam. Informativo, fora da assinatura. |
O corpo é o envelope do evento, serializado uma única vez pela Wolf:
{"id":"evt_...","type":"payment.paid","version":1,"seller_id":"sel_...","resource_id":"pi_...","created_at":"2026-09-27T12:00:00.000Z","data":{...}}Como a assinatura é calculada
signed_payload = x-wolf-timestamp + "." + corpo_bruto
x-wolf-signature = "v1=" + hex( HMAC_SHA256( chave = signing_secret, mensagem = signed_payload ) )- A chave são os bytes UTF-8 do signing secret inteiro, incluindo o prefixo
whsec_live_. - O corpo bruto são exatamente os bytes recebidos. Não faça
JSON.parse+JSON.stringifyantes de verificar: qualquer diferença de espaço ou ordem de chaves invalida a assinatura. Configure o framework para entregar o corpo cru nessa rota. - Compare em tempo constante (
timingSafeEqual,hmac.compare_digest,hash_equals). - Recuse timestamps fora de uma janela de 5 minutos (300 s) em relação ao seu relógio para barrar replay de requisições capturadas. O timestamp é gerado a cada tentativa, então retries e replays legítimos sempre chegam com timestamp atual.
- Há um único valor
v1=por requisição.
Vetor de teste
Use para validar sua implementação (o segredo é fictício):
signing_secret = whsec_live_EXEMPLO_nao_use_em_producao
x-wolf-timestamp = 1790510400
corpo_bruto = {"id":"evt_0000000000000000000000000000abcd","type":"webhook.test","version":1,"seller_id":"sel_exemplo","resource_id":"whe_exemplo","created_at":"2026-09-27T12:00:00.000Z","data":{"endpoint_id":"whe_exemplo","test":true,"message":"Wolf webhook test event."}}
x-wolf-signature = v1=ebf4b0f7f5fff401df7fd114a15c3165ee62668e8f972f5ec6a5cc9454e44c37(Ignore a janela de 5 minutos ao testar com este vetor.)
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.WOLF_WEBHOOK_SECRET; // whsec_live_...
const TOLERANCE_SECONDS = 300;
export function verifyWolfSignature(rawBody, timestamp, signature, secret = SECRET, nowSeconds = Date.now() / 1000) {
if (!/^\d+$/.test(timestamp ?? '')) return false;
if (Math.abs(nowSeconds - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(signature ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// express.raw mantém o corpo como Buffer, exatamente como chegou.
app.post('/wolf/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWolfSignature(req.body, req.get('x-wolf-timestamp'), req.get('x-wolf-signature'))) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
// Idempotência: ignore event.id já processado. Responda 2xx rápido e processe em segundo plano.
res.sendStatus(204);
});Python (Flask)
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
SECRET = os.environ["WOLF_WEBHOOK_SECRET"].encode("utf-8") # whsec_live_...
TOLERANCE_SECONDS = 300
def verify_wolf_signature(raw_body: bytes, timestamp: str, signature: str, secret: bytes = SECRET) -> bool:
if not timestamp or not timestamp.isdigit():
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
digest = hmac.new(secret, timestamp.encode("ascii") + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("v1=" + digest, signature or "")
app = Flask(__name__)
@app.post("/wolf/webhooks")
def wolf_webhook():
raw_body = request.get_data() # bytes, antes de qualquer parse de JSON
if not verify_wolf_signature(raw_body, request.headers.get("x-wolf-timestamp", ""), request.headers.get("x-wolf-signature", "")):
abort(400)
event = request.get_json()
# Idempotência: ignore event["id"] já processado.
return "", 204PHP
<?php
const TOLERANCE_SECONDS = 300;
function verify_wolf_signature(string $rawBody, string $timestamp, string $signature, string $secret): bool
{
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {
return false;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
$secret = getenv('WOLF_WEBHOOK_SECRET'); // whsec_live_...
$rawBody = file_get_contents('php://input'); // bytes exatos do corpo
$timestamp = $_SERVER['HTTP_X_WOLF_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WOLF_SIGNATURE'] ?? '';
if (!verify_wolf_signature($rawBody, $timestamp, $signature, $secret)) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
// Idempotência: ignore $event['id'] já processado.
http_response_code(204);Respostas, retries e replays
- Sucesso é qualquer
2xxem até 10 s. Outros status, timeout, erro de TLS, resposta maior que 64 KiB ou redirect que não seja307/308contam como falha. Redirects307/308são seguidos até 3 vezes, revalidando DNS e IP a cada salto; destinos privados ou da própria Wolf são recusados. - Retries automáticos: até 5 tentativas por endpoint e evento, com esperas de 1 min, 5 min, 30 min e 2 h entre elas; depois a entrega fica
dead_letterno painel. - Replay e teste (painel ou
POST /v1/webhooks/deliveries/:id/replayePOST /v1/webhooks/endpoints/:id/test) fazem uma tentativa, assinada da mesma forma. O replay reenvia o mesmo evento, com o mesmox-wolf-event-idewolf-delivery-attemptmaior. O evento de teste temtype: "webhook.test". - Ordem não é garantida e a mesma tentativa pode chegar mais de uma vez: trate cada
event_idde forma idempotente. - Auto-desativação: após 100 falhas consecutivas atribuíveis ao seu receptor o endpoint é desativado e o painel mostra um aviso. Um sucesso zera o contador. Reative pelo painel ou com
PATCH /v1/webhooks/endpoints/:id({"status":"active"}).
Rotação do signing secret
POST /v1/webhooks/endpoints/:id/rotate-secret (ou "Rotacionar signing secret" no painel) gera um segredo novo, exibido uma única vez. A troca é imediata e não há período em que os dois segredos valham: atualize o segredo no receptor logo em seguida. Entregas que já estavam em andamento no momento da rotação podem chegar assinadas com o segredo anterior; o retry seguinte usa o novo. Se o segredo vazar, rotacione e, se preciso, desative o endpoint até concluir a troca.