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.

Entrega pelo menos uma vez. O mesmo evento pode chegar mais de uma vez e fora de ordem. Deduplique pelo 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.

POST /v1/webhooks
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"
  }'
Resposta · 201
{
  "id": "whk_...",
  "url": "https://sualoja.com.br/webhooks/qorvie-pay",
  "eventos": ["cobranca.*", "checkout.pago"],
  "ativo": true,
  "segredo": "whsec_..."
}
  • O segredo aparece só nesta resposta. Guarde-o no servidor que recebe os avisos; ele serve para conferir a assinatura.
  • eventos aceita * (todos), um grupo como cobranca.* ou tipos exatos. Padrão: *.
  • A URL precisa ser https e 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

TipoQuando
cobranca.criadaUma cobrança foi criada (o payload traz o status inicial).
cobranca.autorizadaCartão autorizado, ainda não capturado.
cobranca.pagaPagamento confirmado.
cobranca.recusadaCartão recusado.
cobranca.expiradaO Pix venceu sem pagamento.
cobranca.estornadaEstorno concluído.
cobranca.contestadaChargeback aberto.
checkout.pagoUm link de pagamento foi pago. Traz o id do link e a sua referência.
recebedor.criadoUm recebedor foi cadastrado.
recebedor.atualizadoO status de verificação (KYC) do recebedor mudou.
webhook.testeEvento 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

POST https://sualoja.com.br/webhooks/qorvie-pay
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.

  1. Leia o corpo bruto da requisição, antes de qualquer parse de JSON.
  2. Separe t e os valores v1 do cabeçalho QorviePay-Signature.
  3. Recuse se t estiver a mais de 5 minutos de agora (barra replay).
  4. Calcule o HMAC e compare com cada v1 em tempo constante. Basta um bater.
Node.js (Express)
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()
})
Python
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")
PHP
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 com POST /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 um webhook.teste só para aquele endpoint.
  • GET /v1/webhooks/entregas lista as entregas com status, tentativas e o histórico de resultados. Filtre por endpointId e status (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 id do evento (ou por QorviePay-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.