Pular para o conteúdo principal

Webhooks

É assim que você sabe que algo aconteceu, sem perguntar em laço.

Dois canais, e eles convivem​

CanalAlcanceQuando usar
endereço da contatodas as transações, inscrito numa lista de eventoscanal permanente, para conciliação
callbackUrl da transaçãosó os eventos daquela transaçãoquando você tem um servidor por pedido

A callbackUrl dispensa cadastro prévio: você a informa na chamada que cria a cobrança ou o saque.

Quem usa os dois recebe duas entregas por evento, com X-Webhook-Id diferentes. É a combinação que se escolhe de propósito, e suprimir uma por achar que é duplicidade quebraria esse desenho. Para deduplicar, use o id da transação no corpo.

O que chega​

{ "type": "charge.paid", "data": { } }

data é o mesmo objeto que a API devolve — Charge em eventos de cobrança, Payout em eventos de saque. Você não precisa de um parser diferente.

Eventos​

EventoQuando
charge.paida cobrança foi paga ou a compra foi aprovada
charge.settledo dinheiro do cartão ficou disponível
charge.declinedrecusada (o pagador pode tentar de novo)
charge.expiredo prazo acabou sem pagamento: ninguém tentou pagar
charge.refundeddevolvida — e também contestada, com data.refund.isChargeback: true
payout.settledsaque pago
payout.failedsaque falhou; a retenção foi devolvida

Cabeçalhos​

CabeçalhoConteúdo
X-Webhook-Signaturet=<epoch>,v1=<hmac-sha256 hex>
X-Webhook-Ididentificador do evento, estável entre retentativas
X-Webhook-Evento mesmo valor de type

Verificar a assinatura​

A assinatura cobre "<timestamp>.<corpo cru>".

O instante entra dentro do que é assinado. Sem ele, quem capturasse uma entrega legítima poderia reenviá-la para sempre, e cada reenvio passaria na sua verificação.

[$t, $v1] = sscanf($request->header('X-Webhook-Signature'), 't=%d,v1=%s');

$esperado = hash_hmac('sha256', $t . '.' . $request->getContent(), $segredo);

if (! hash_equals($esperado, $v1)) {
abort(400);
}

if (abs(time() - $t) > 300) {
abort(400); // entrega antiga demais — provável reenvio
}

:::danger Verifique sobre o corpo cru Antes de qualquer parsing. Reserializar o JSON muda os bytes — a ordem das chaves, um espaço — e a assinatura deixa de conferir.

Em frameworks que fazem parsing automático do corpo, procure o acesso ao conteúdo bruto: $request->getContent(), request.body cru, req.rawBody. :::

Qual segredo​

CanalSegredo
endereço da contao whsec_… mostrado uma vez, na criação
callbackUrlo segredo da credencial que criou a transação

No segundo caso você já o tem — e revogar a chave derruba os dois sentidos de uma vez.

Responder​

Esperamos 2xx. Qualquer outra coisa entra em retentativa.

Responda rápido e processe em segundo plano. O tempo limite é de 10 segundos.

Retentativas​

Recuo exponencial: 10s, 30s, 2min, 10min, 1h, 6h. Depois disso, a entrega é marcada como esgotada.

A escala é grande de propósito: a causa mais comum de falha é o seu servidor estar fora, e insistir de dez em dez segundos por seis horas não o traz de volta.

Entrega é ao menos uma vez​

Deduplique por X-Webhook-Id. Ele é estável entre retentativas, então o mesmo evento entregue três vezes traz o mesmo id nas três.

O caminho mais simples: guarde os ids processados e ignore repetidos.

O que você não vai ver​

Falha de entrega nunca desfaz o que aconteceu. Se o seu servidor esteve fora por seis horas, a cobrança continua paga e o saldo continua creditado — o que se perdeu foi o aviso, não o fato. Concilie por extrato.