Webhooks

Estratégia de novas tentativas de webhooks em sistemas de pagamento

Como a Qvard repete um webhook de pagamento, o que conta como entrega falha e como construir um tratador que sobreviva a repetições, duplicatas e quedas sem cumprir um pedido duas vezes.

Um webhook de pagamento é uma mensagem que não pode se perder e não pode ser processada duas vezes. Redes falham e servidores reiniciam, então o remetente repete; é por isso que seu tratador precisa ser escrito pensando em repetições. Este guia descreve como a Qvard repete, o que ela conta como falha e como construir um tratador e uma rotina de recuperação em torno desse comportamento.

Quando um webhook é enviado

A Qvard envia o webhook quando um pagamento está concluído. É um POST para a callback URL salva no seu projeto, com um JSON pequeno contendo o seu id (o identificador que você informou ao criar o pagamento) e o status, assinado no cabeçalho X-Qvard-Signature.

Um pagamento a menor, rejeitado ou ainda aguardando não gera webhook de conclusão. Se você precisa saber desses pagamentos, olhe-os no painel; não espere uma mensagem que não é enviada.

O que conta como entregue

A Qvard considera uma entrega bem-sucedida somente quando seu endpoint responde exatamente HTTP 200. Todo o resto é falha:

  • qualquer outro código de status, incluindo 201, 202, 204 e todos os 4xx e 5xx;
  • um timeout, porque seu servidor foi lento ou ficou inacessível;
  • um redirecionamento: a Qvard não segue 301 nem 302, então uma regra que redireciona http para https ou acrescenta uma barra final faz a entrega falhar.

Essa precisão é deliberada. Um 204 No Content parece uma resposta perfeitamente boa e ainda assim é uma entrega falha. Devolva 200, com corpo vazio se quiser.

Como funcionam as novas tentativas

Depois de uma entrega falha, a Qvard tenta de novo. No total são cinco tentativas em cerca de seis minutos. Duas consequências:

  • Uma queda curta é coberta. Se seu servidor reinicia ou um deploy leva um ou dois minutos, uma tentativa posterior tem sucesso e você não perde nada.
  • Uma queda mais longa não. Após a última tentativa a Qvard para. Se seu servidor ficou fora do ar por mais que esses seis minutos, a notificação não chega sozinha; reenvie-a para esse pagamento pelo painel.

Por causa das repetições, seu tratador às vezes verá o mesmo evento mais de uma vez, por exemplo quando seu servidor fez o trabalho mas a resposta se perdeu na volta. O exemplo abaixo foi construído para isso: a atualização só altera um pedido ainda não pago, o cumprimento só é enfileirado quando uma linha mudou e a resposta é 200 de qualquer forma.

Pythonwebhook.py
import os
from fastapi import FastAPI, Request, Response
from qvard import Qvard

app = FastAPI()
SECRET = os.environ["QVARD_SECRET_KEY"]
qvard = Qvard(SECRET)

@app.post("/webhooks/qvard")
async def qvard_webhook(request: Request):
    try:
        body = qvard.webhooks.construct_event(await request.body(), request.headers.get("X-Qvard-Signature", ""), SECRET)
    except ValueError:
        return Response(status_code=400)

    order_id, status = body.get("id"), body.get("status")

    # Idempotent: the UPDATE changes a row only for an order that is not paid yet.
    if status == "completed":
        changed = await db.execute(
            "UPDATE orders SET state = 'paid' WHERE id = :id AND state <> 'paid'", {"id": order_id}
        )
        if changed:
            await queue.enqueue("fulfil", order_id)   # slow work after the answer

    return Response(status_code=200)   # exactly 200, also for repeats and unknown orders

O que seu tratador deve fazer

Quatro regras cobrem quase todos os problemas:

  1. Verificar a assinatura. Compare o cabeçalho X-Qvard-Signature (HMAC-SHA256 com a sua chave secreta sobre o timestamp e o corpo bruto) em tempo constante e responda 400 caso contrário. Verifique-a antes de agir sobre o pagamento. Veja como proteger webhooks de pagamentos em cripto.
  2. Ser idempotente. Torne condicional a mudança de estado, «marcar pago se ainda não estiver pago», idealmente em uma única instrução de banco de dados, para que duas entregas paralelas não vençam ambas.
  3. Responder rápido. Conferir, atualizar, enfileirar e responder exatamente 200. Faça e-mails, geração de arquivos e chamadas a outros sistemas a partir de uma fila, para que um serviço lento não vire timeout no webhook.
  4. Responder 200 ao que você nunca vai tratar. Um pedido desconhecido ou uma repetição também devem devolver 200; do contrário a Qvard continua repetindo uma mensagem que você já tratou ou nunca tratará.

Falhas que parecem bugs da Qvard

A maioria dos relatos de «o webhook não chegou» tem causa no lado receptor:

  • Um firewall ou WAF bloqueando requisições de endereços desconhecidos, ou uma página de «em breve» ou de manutenção.
  • Um redirecionamento de http para https, de www para o domínio sem prefixo ou para uma barra final.
  • Um erro do framework depois de a resposta já estar decidida, por exemplo uma exceção em um middleware que transforma seu 200 em 500.
  • Um tratador lento que faz o trabalho antes de responder e esbarra em um timeout.
  • Um cache ou CDN na frente da rota.
  • A URL errada salva no projeto, ou um erro de digitação na rota.

Teste o endpoint de fora da sua rede com uma requisição que leve um cabeçalho X-Qvard-Signature válido e confira no log de acesso do servidor o código da resposta, não apenas se a requisição chegou.

Uma rotina de recuperação para notificações perdidas

As novas tentativas cobrem quedas curtas; para o resto, adicione uma rede de segurança.

  • Registre cada entrega com o id, o status, sua decisão (aplicado, repetição, pedido desconhecido, chave rejeitada) e a hora. Uma linha ausente no log é como você percebe uma lacuna.
  • Concilie em horário fixo. Uma vez por dia, compare os pedidos que você ainda considera não pagos com os pagamentos e o saldo no painel da Qvard. Tudo que o painel mostra como concluído e você como não pago é uma notificação perdida.
  • Reenvie pelo painel esses pagamentos e deixe o tratador idempotente aplicá-los.
  • Alerte quando uma sequência de requisições receber erro, para saber de um endpoint quebrado em minutos e não no fim do dia.

O restante da integração, incluindo a criação do pagamento, está em como aceitar pagamentos em cripto em um site e API de pagamentos em cripto para desenvolvedores.

FAQ

Quantas vezes a Qvard repete um webhook?

São cinco tentativas no total em cerca de seis minutos. Depois disso a Qvard para, e você pode reenviar a notificação do pagamento pelo painel.

Que código de status meu endpoint deve devolver?

Exatamente 200. Qualquer outro status, incluindo 204, um timeout ou um redirecionamento conta como entrega falha e é repetido.

O mesmo webhook pode chegar duas vezes?

Sim, por exemplo quando seu servidor processou a primeira entrega mas a resposta se perdeu. Torne o tratador idempotente e ainda assim responda 200 às repetições.

Recebo um webhook para um pagamento a menor ou rejeitado?

Não. O webhook é enviado quando um pagamento é concluído. Confira os demais pagamentos no painel.

Artigos relacionados

Artigos relacionados