Como confirmar automaticamente o pagamento de um pedido por webhook

Vincule a fatura ao pedido, verifique a assinatura do webhook e proteja a entrega contra repetições, pagamentos insuficientes e reversões.

Atualizado:

Para que o site marque um pedido como pago automaticamente, crie uma fatura para ele no Cryptanio e configure um webhook: uma notificação que o serviço envia ao seu servidor quando o estado do pagamento muda. O servidor verifica a autenticidade da mensagem, encontra o pedido e confere o estado da fatura. Depois disso, pode liberar o acesso comprado ou encaminhar o pedido para execução.

Vamos usar o exemplo de uma loja que cria uma fatura em dólares americanos como unidade de cálculo, enquanto o comprador escolhe a criptomoeda e a rede disponíveis na página de pagamento. Os fundos chegam aos endereços da loja. Os pedidos e a entrega do produto continuam no aplicativo da loja, enquanto o Cryptanio associa a fatura à transferência e informa seu estado. Para essa integração, será necessário um desenvolvedor ou a possibilidade de alterar a parte do site executada no servidor.

Prepare o projeto e os endereços para pagamento

No painel do Cryptanio, escolha um projeto e adicione endereços de recebimento que pertençam a você. Conecte-os ao serviço Payments: apenas monitorar um endereço não é suficiente para emitir faturas. Certifique-se de que os endereços estejam ativos e correspondam à rede escolhida. Para pagamentos reais, são necessários endereços do modo de produção, live.

Prepare vários endereços se os clientes puderem pagar pedidos ao mesmo tempo. Um endereço pode estar ocupado por uma fatura aberta ou ficar temporariamente indisponível após o encerramento dela. A integração deve mostrar um erro claro quando não houver endereço livre e permitir uma nova tentativa mais tarde.

Crie uma chave de API e salve-a no servidor da loja. Ela será necessária para as chamadas a https://api.cryptanio.com/v1. A chave não pode ser colocada no JavaScript da página, em um aplicativo móvel nem em um repositório público.

Configure o endereço de recebimento das notificações

Abra as “Notificações” do projeto escolhido. Se “Usar configurações de notificação de conta” estiver habilitado, configure Webhook em “Configurações da conta”, ou desative a herança e defina o endereço próprio do projeto.

No cartão Webhook, informe o endereço HTTPS do receptor em “URL do ponto final” e clique em “Salvar”. O receptor deve aceitar solicitações POST diretamente: não é utilizado redirecionamento para outra URL. Obtenha o “Segredo de assinatura” no mesmo cartão e guarde-o no servidor, separado da chave de API.

Confirme que Webhook não está pausado. Trate payment.confirmed e payment.rolled_back no receptor e use payment.pending para um estado intermediário, se necessário. Antes de executar o pedido, confira o modo real e as demais condições abaixo. Não é preciso ativar um canal Webhook separado para o endereço da fatura.

Crie uma fatura para um pedido específico

Primeiro, salve o pedido no banco de dados da loja. Em seguida, o servidor cria uma fatura por POST /v1/invoices. Em reference, envie seu identificador do pedido. Neste exemplo, o preço do pedido é 12.50 USD: invoice_currency e invoice_amount definem o preço, enquanto o comprador escolhe a rede e o ativo disponíveis na página de pagamento. Por isso, chain, asset, amount, address e tolerance não são enviados nessa solicitação. Informar USD não significa receber dinheiro fiduciário nem converter a criptomoeda recebida em dólares.

#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
  local text="$1" char code index output='"'
  for ((index=0; index<${#text}; index++)); do
    char="${text:index:1}"
    case "$char" in
      '"'|'\') output+="\\$char" ;;
      *) printf -v code '%d' "'$char"
         if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
         output+="$char" ;;
    esac
  done
  printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
  --silent --show-error --max-time 30 --request POST \
  --header "Authorization: Bearer $CRYPTANIO_API_KEY" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
  --data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
  "https://api.cryptanio.com/v1/invoices" \
  --output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
  cat "$response_file"
  exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1

Transferir exemplo

Antes de executar, defina no servidor CRYPTANIO_API_KEY, CRYPTANIO_PROJECT_ID e CRYPTANIO_OPERATION_ID. Use uma chave de API de produção com permissão para criar faturas; CRYPTANIO_PROJECT_ID deve apontar para o projeto Payments preparado. CRYPTANIO_OPERATION_ID é o identificador salvo da operação de criação para esse pedido: de 16 a 80 letras latinas ASCII, números, hífens ou sublinhados. O exemplo o envia no cabeçalho Idempotency-Key, não em um campo JSON. Para um novo pedido, crie outro valor; para repetir a mesma solicitação, mantenha o anterior.

Se a criação for bem-sucedida, a API retorna HTTP 201. Salve o id da fatura e a checkout_url recebida junto ao pedido e encaminhe o comprador para esse link. Enquanto a forma de pagamento não for escolhida, a rede, o endereço e o valor em criptomoeda ainda não estarão definidos. Na página de pagamento, o comprador escolhe uma opção disponível e recebe os dados atualizados e o prazo para pagar. Se você exibir os dados de pagamento na própria interface, use os dados da tentativa específica, o valor exato de amount e o prazo expires_at; não arredonde o valor nem o substitua por asked. Os links de consultas GET posteriores e da lista de faturas são destinados à visualização; guarde separadamente o link interativo recebido na resposta de criação e compartilhe-o apenas com o comprador correspondente.

Se a conexão cair após o envio da solicitação, reenvie o mesmo corpo com a mesma Idempotency-Key. Uma repetição bem-sucedida retorna HTTP 201 e o mesmo id. Alterar os parâmetros com uma chave já utilizada causa HTTP 409. reference associa a fatura ao pedido, mas não substitui a chave de criação única. Não altere a chave apenas por causa de um tempo limite excedido.

Verifique a autenticidade do webhook recebido

O Cryptanio envia X-Timestamp, X-Signature, X-Event-Id, X-Event-Type e X-Attempt. A assinatura é calculada assim:

hex(HMAC-SHA256(secret, timestamp + "." + raw_body))

raw_body são os bytes originais da solicitação. Obtenha-os antes de interpretar o JSON: serializá-lo novamente pode alterar os espaços ou a ordem dos campos e invalidar a verificação. Limite o tamanho permitido para o corpo e rejeite solicitações sem uma assinatura válida.

O exemplo abaixo usa uma janela de validade de cinco minutos. Essa é uma configuração do seu receptor; para funcionar corretamente, o relógio do servidor precisa estar sincronizado.

import hashlib
import hmac
import re
import time

def verify_signature(secret, timestamp, raw_body, signature):
    if not secret or not timestamp or not re.fullmatch(r"[0-9]{1,12}", timestamp):
        return False
    if not signature or not re.fullmatch(r"[0-9a-f]{64}", signature):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    signed = timestamp.encode("ascii") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

A função verifica a assinatura e o horário da solicitação. Depois de uma verificação bem-sucedida, interprete o JSON e confirme schema_version="v1", a project_id esperada, mode="live" e sandbox=false.

Para tomar decisões, use event_id e event_type do corpo assinado. Os cabeçalhos com os mesmos nomes podem ser comparados a esses valores; esses cabeçalhos, por si só, não são protegidos pela assinatura. Execute a lógica de pagamento apenas para o objeto payment esperado, com a direção in.

Separe a confirmação da transferência do pagamento do pedido

Os principais eventos para a loja são:

  • payment.pending — a transferência foi detectada e ainda aguarda confirmação;
  • payment.confirmed — a transferência atingiu as confirmações necessárias;
  • payment.rolled_back — uma transferência detectada anteriormente foi revertida devido a uma mudança na blockchain.

O evento payment.confirmed ainda não significa que um pedido específico foi totalmente pago. Verifique o objeto invoice e compare invoice.id e invoice.reference com o pedido salvo. Uma mensagem sem uma fatura vinculada não pode ser atribuída automaticamente a uma compra apenas por ter um valor semelhante.

Antes da entrega, consulte o estado atual por GET /v1/invoices/{id}. Confira id, project_id, reference, o preço do pedido em invoice_currency e invoice_amount, além de sandbox=false. Verifique a rede, o ativo, o endereço e a tentativa de pagamento escolhida; se houver várias tentativas, considere settled_attempt_id e settled_attempt. Um pagamento bem-sucedido corresponde ao status de fatura paid; pending, seen e partial ainda não autorizam a entrega. O retorno do comprador ao site ou a abertura do link de pagamento não confirmam o pagamento.

Para pagamentos inferiores ao valor devido, existe underpaid; para um pagamento parcial com espera pelo restante, partial; para entradas atrasadas, late. Em caso de pagamento excedente, a política do projeto pode manter a fatura em paid ou definir overpaid com needs_decision=true. Os estados underpaid, overpaid e late exigem uma decisão separada antes da entrega do pedido. Os campos amount_raw e paid chegam como strings. Para a comparação, interprete-os como valores inteiros exatos nas unidades mínimas do ativo; decimals define a escala. Não compare as strings lexicograficamente nem use ponto flutuante binário. Defina previamente as regras para tratar a diferença; isso não implica uma devolução automática dos fundos.

Trate repetições e reversões

A entrega admite mensagens repetidas. Salve o evento verificado em armazenamento persistente e use event_id como chave única. Uma repetição de um evento já aceito deve receber uma resposta de sucesso sem provocar outra entrega do produto.

Apenas o identificador da transação não basta para descartar todas as mensagens posteriores: a transferência passa por estados diferentes. Para a combinação de chain, tx_id, index, address e a identidade do ativo, mantenha a última payment.revision aplicada. Aplique a transição somente se a revision for maior que a já processada.

Registre a alteração do pedido e a criação da tarefa de entrega de forma consistente, com proteção contra processamento paralelo. A tarefa deve ter uma chave única do pedido. Imediatamente antes da execução, verifique se a entrega do pedido ainda está autorizada. Se chegar uma reversão, atualize o estado e interrompa a entrega ainda não executada; para um acesso já liberado, preveja uma revisão ou suspensão.

Verifique todo o fluxo

Antes de um pagamento real, teste a leitura do corpo, a assinatura e a proteção contra duplicatas com dados preparados. Um evento de serviço webhook.test não confirma uma compra nem substitui a verificação do fluxo completo.

Depois, confira todo o processo com um pedido de controle próprio: criação da fatura, transferência, confirmação e atualização do pedido. Teste separadamente a repetição de um evento, um pagamento inferior ao devido e uma reversão com dados preparados. Retorne HTTP 2xx depois de salvar de forma confiável a mensagem aceita; se houver uma impossibilidade temporária de salvá-la, use um erro de servidor para permitir uma nova tentativa de entrega.

No painel, compare o evento e o registro de entrega com o registro no seu aplicativo. A verificação termina quando o pedido confirmado é executado uma única vez e um evento intermediário, alheio ao pedido ou repetido não provoca uma entrega indevida.

Referência: faturas da API v1, verificação da assinatura do webhook.