Webhooks
Avisamos o seu servidor, por HTTP, quando algo muda. Assim você não precisa ficar consultando a API para saber se um Pix foi pago.
id do evento e, se a ordem importar, confirme o estado atual com GET /v1/cobrancas/{id}.1. Cadastre um endpoint
No painel (Webhooks) ou pela API. Cada ambiente tem os seus: endpoints do sandbox só recebem eventos do sandbox, e os de produção só os de produção.
curl -X POST https://api.pay.qorvie.com/v1/webhooks \
-H "Authorization: Bearer $CHAVE_SECRETA" \
-H "Content-Type: application/json" \
-d '{
"url": "https://sualoja.com.br/webhooks/qorvie-pay",
"eventos": ["cobranca.*", "checkout.pago"],
"descricao": "Produção da loja"
}'{
"id": "whk_...",
"url": "https://sualoja.com.br/webhooks/qorvie-pay",
"eventos": ["cobranca.*", "checkout.pago"],
"ativo": true,
"segredo": "whsec_..."
}- O
segredoaparece só nesta resposta. Guarde-o no servidor que recebe os avisos; ele serve para conferir a assinatura. eventosaceita*(todos), um grupo comocobranca.*ou tipos exatos. Padrão:*.- A URL precisa ser
httpse pública. Endereços de rede interna (10.x, 192.168.x, localhost, metadados de nuvem) são recusados, no cadastro e de novo a cada envio. - Até 5 endpoints por ambiente.
Eventos
| Tipo | Quando |
|---|---|
cobranca.criada | Uma cobrança foi criada (o payload traz o status inicial). |
cobranca.autorizada | Cartão autorizado, ainda não capturado. |
cobranca.paga | Pagamento confirmado. |
cobranca.recusada | Cartão recusado. |
cobranca.expirada | O Pix venceu sem pagamento. |
cobranca.estornada | Estorno concluído. |
cobranca.contestada | Chargeback aberto. |
checkout.pago | Um link de pagamento foi pago. Traz o id do link e a sua referência. |
recebedor.criado | Um recebedor foi cadastrado. |
recebedor.atualizado | O status de verificação (KYC) do recebedor mudou. |
webhook.teste | Evento de teste, enviado só quando você pede. |
A lista atual também sai de GET /v1/webhooks/tipos. Um cartão aprovado gera cobranca.criada e logo em seguida cobranca.paga.
O que chega no seu servidor
QorviePay-Signature: t=1760619600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56e...
QorviePay-Event-Id: evt_...
QorviePay-Event-Type: cobranca.paga
QorviePay-Delivery-Id: ent_...
QorviePay-Delivery-Attempt: 1
Content-Type: application/json
{
"id": "evt_9c6ca81b333a4c548eed5ad6ff693262",
"tipo": "cobranca.paga",
"criadoEm": "2026-10-16T12:00:00.000Z",
"ambiente": "teste",
"dados": {
"cobranca": {
"id": "ch_...",
"status": "paga",
"metodo": "pix",
"valorCentavos": 25000,
"referencia": "pedido-1001",
"distribuicao": { "partes": [{ "recebedorId": "rc_...", "valorCentavos": 22500 }], "plataformaCentavos": 2500 }
}
}
}Os dados são o retrato da cobrança no momento do evento, no mesmo formato da API. Em checkout.pago, dados.checkout traz o id do link e a sua referencia.
2. Confira a assinatura
Sem conferir, qualquer pessoa que descobrir a sua URL poderia fingir um pagamento. A assinatura é um HMAC-SHA256 de <t>.<corpo>, em hexadecimal, usando o segredo inteiro (com o prefixo whsec_) como chave.
- Leia o corpo bruto da requisição, antes de qualquer parse de JSON.
- Separe
te os valoresv1do cabeçalhoQorviePay-Signature. - Recuse se
testiver a mais de 5 minutos de agora (barra replay). - Calcule o HMAC e compare com cada
v1em tempo constante. Basta um bater.
import { createHmac, timingSafeEqual } from 'node:crypto'
function conferir(segredo, cabecalho, corpoBruto) {
const partes = cabecalho.split(',').map((p) => p.trim().split('='))
const t = Number(partes.find(([k]) => k === 't')?.[1])
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false
const esperada = Buffer.from(createHmac('sha256', segredo).update(`${t}.${corpoBruto}`).digest('hex'))
return partes.filter(([k]) => k === 'v1').some(([, v]) => {
const recebida = Buffer.from(v ?? '')
return recebida.length === esperada.length && timingSafeEqual(recebida, esperada)
})
}
app.post('/webhooks/qorvie-pay', express.raw({ type: 'application/json' }), (req, res) => {
const corpo = req.body.toString('utf8')
if (!conferir(process.env.QORVIE_PAY_WEBHOOK_SECRET, req.header('QorviePay-Signature') ?? '', corpo)) {
return res.status(401).end()
}
const evento = JSON.parse(corpo)
// 1) deduplique por evento.id 2) enfileire o trabalho 3) responda logo
res.status(200).end()
})import hashlib, hmac, time
def conferir(segredo: str, cabecalho: str, corpo: bytes) -> bool:
partes = [p.strip().split("=", 1) for p in cabecalho.split(",")]
t = next((v for k, v in partes if k == "t"), None)
if t is None or abs(time.time() - int(t)) > 300:
return False
esperada = hmac.new(segredo.encode(), f"{t}.".encode() + corpo, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, esperada) for k, v in partes if k == "v1")function conferir(string $segredo, string $cabecalho, string $corpo): bool {
$t = null; $assinaturas = [];
foreach (explode(',', $cabecalho) as $p) {
[$k, $v] = array_pad(explode('=', trim($p), 2), 2, '');
if ($k === 't') $t = (int) $v;
if ($k === 'v1') $assinaturas[] = $v;
}
if ($t === null || abs(time() - $t) > 300) return false;
$esperada = hash_hmac('sha256', $t . '.' . $corpo, $segredo);
foreach ($assinaturas as $a) if (hash_equals($esperada, $a)) return true;
return false;
}Resposta e novas tentativas
- Responda 2xx em até 10 segundos. O corpo da resposta é ignorado. Faça o trabalho pesado depois, numa fila.
- Qualquer outra coisa (erro 4xx/5xx, timeout, conexão recusada, redirecionamento) conta como falha. Não seguimos redirecionamentos.
- Falhou? Tentamos de novo depois de 1 min, 5 min, 15 min, 1 h, 3 h, 6 h e 12 h (com uma variação de ±10%). São 8 tentativas ao todo, cerca de 22 horas.
- Esgotadas as tentativas, a entrega fica como
falhou. Reenvie pelo painel ou comPOST /v1/webhooks/entregas/{id}/reenviar. - Todas as tentativas de um evento levam o mesmo corpo; só mudam o carimbo e a assinatura. O número da tentativa vem em
QorviePay-Delivery-Attempt.
Teste e acompanhamento
POST /v1/webhooks/{id}/teste(ou o botão "Enviar teste" no painel) envia umwebhook.testesó para aquele endpoint.GET /v1/webhooks/entregaslista as entregas com status, tentativas e o histórico de resultados. Filtre porendpointIdestatus(pendente,entregue,falhou).- Com o sandbox, crie cobranças e use a simulação de pagamento para ver os avisos chegando.
Trocar o segredo sem perder avisos
POST /v1/webhooks/{id}/rotacionar gera um segredo novo. Durante 24 horas o cabeçalho traz duas assinaturas (v1 do segredo novo e v1 do antigo), então o seu servidor aceita qualquer um enquanto você troca a configuração. Passado o prazo, só o novo vale.
Boas práticas
- Guarde o segredo como qualquer credencial e nunca o registre em log.
- Deduplique pelo
iddo evento (ou porQorviePay-Event-Id). - Não confie na ordem dos eventos. Antes de liberar um produto, confirme o status da cobrança na API ou use o evento
checkout.pago. - Responda rápido. Quem demora mais de 10 segundos recebe o mesmo evento de novo.