Ambiente de simulação — nenhuma cobrança real. Os pagamentos, Pix e cartões deste sandbox são sintéticos.
W Wolf Sandbox Docs Criar conta
Navegação

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 endpoint live exige 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_events vazio recebe todos os eventos; com itens, só os tipos listados. O evento webhook.test ignora o filtro.

Operações

Eventos

O corpo é o envelope do evento. data nunca traz dados do pagador (nome, e-mail ou documento).

Exemplo de corpo: payment.paid json
{
  "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"
  }
}
TipoVersãoQuando
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

EstadoSignificadoPode 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 disabled e o painel mostra um aviso. Falhas da própria Wolf não contam.
  • Testar (/test) e reenviar (/replay) exigem endpoint active (senão 409 WEBHOOK_ENDPOINT_DISABLED).

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.

Endpoints simulated não recebem requisições HTTP: a entrega é só registrada no painel. Produção entrega apenas em modo live.

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.

HeaderExemploSignificado
content-typeapplication/jsonSempre JSON.
user-agentWolfPayment-Webhooks/2Identifica o remetente (não é prova de autenticidade).
x-wolf-event-idevt_...Id estável do evento: o mesmo em retries, replays e testes reenviados. Use-o para deduplicar.
x-wolf-event-typepayment.paid, webhook.testTipo do evento (igual a type no corpo).
x-wolf-timestamp1790510400Segundos Unix (UTC) do momento da assinatura desta tentativa.
x-wolf-signaturev1=ebf4b0f7...4c37v1= seguido do HMAC-SHA256 em hexadecimal minúsculo (64 caracteres).
wolf-delivery-attempt1Nú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:

JSON
{"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

Texto
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.stringify antes 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):

Texto
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)

Node.js
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)

Python
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 "", 204

PHP

PHP
<?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 2xx em até 10 s. Outros status, timeout, erro de TLS, resposta maior que 64 KiB ou redirect que não seja 307/308 contam como falha. Redirects 307/308 sã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_letter no painel.
  • Replay e teste (painel ou POST /v1/webhooks/deliveries/:id/replay e POST /v1/webhooks/endpoints/:id/test) fazem uma tentativa, assinada da mesma forma. O replay reenvia o mesmo evento, com o mesmo x-wolf-event-id e wolf-delivery-attempt maior. O evento de teste tem type: "webhook.test".
  • Ordem não é garantida e a mesma tentativa pode chegar mais de uma vez: trate cada event_id de 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.