Webhooks

Os eventos que o Aliquot envia, seus payloads, como verificar a assinatura, o calendário de novas tentativas, como reenviar uma entrega, e como gerenciar endpoints e rotacionar o segredo de assinatura.

Última atualização 27 de out. de 2026

O Aliquot notifica o seu servidor sobre mudanças em faturas, payouts e reembolsos com requisições HTTPS POST para os endpoints que você registra. Cada requisição é assinada com o segredo do endpoint.

Eventos

EventoEnviado quando
invoice.createdUma fatura é criada, pela API ou a partir de um payment link.
invoice.detectedA primeira transferência para a fatura é vista on-chain (também quando uma fatura paga a menos recebe um complemento). O pagamento está sendo confirmado.
invoice.paidO pagamento atingiu o limiar de confirmações e cobre o total. Também é enviado para faturas pagas a mais, com "status": "OVERPAID". Entregue o pedido neste evento.
invoice.overpaidEnviado junto com invoice.paid quando o comprador enviou mais do que o total. O excedente vira um reembolso.
invoice.underpaidO valor confirmado está abaixo do total. O comprador ainda pode enviar o restante antes de a janela terminar.
invoice.expiredA janela de pagamento terminou: status é EXPIRED (nada recebido) ou EXPIRED_UNDERPAID (um pagamento parcial, retido como reembolso).
payout.completedUma transação de payout atingiu o limiar de confirmações.
payout.failedUma transação de payout foi revertida ou descartada. O valor volta ao saldo disponível.
refund.claimableHá dinheiro retido para um comprador que pode ser resgatado. Traz claim_url.
refund.completedUm reembolso resgatado foi enviado e confirmado.

Anular uma fatura não envia nenhum evento.

Formato da entrega

Toda entrega é um POST com Content-Type: application/json e estes headers:

HeaderValor
X-Webhook-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-Webhook-TimestampO mesmo t, sozinho.
X-Webhook-EventO nome do evento, como invoice.paid.

O corpo é um envelope em torno dos dados do evento:

{
  "id": "evt_9b1f4c2a7d3e8f6a5b0c1d2e",
  "type": "invoice.paid",
  "occurredAt": "2026-10-27T14:03:11.482Z",
  "data": {
    "organization_id": "cm2k8x1qv0000l708h3d9f2ab",
    "invoice_id": "inv_01JB7Q8M3X4K9TPZ2R6WVN5HDC",
    "status": "PAID",
    "livemode": false,
    "merchant_order_id": "order-77310",
    "payment_link_id": null,
    "network": "POLYGON_AMOY",
    "token": "USDC",
    "amount_total": "102.50",
    "amount_received": "102.50"
  }
}
CampoDescrição
idIdentifica o evento. É o mesmo em todas as novas tentativas do mesmo evento, então use-o para processar cada evento uma única vez.
typeO nome do evento.
occurredAtQuando a mudança aconteceu (não quando a entrega foi enviada).
dataO payload do evento, abaixo. Os valores são strings decimais em unidades do token.

Todo objeto data usa nomes de campo em snake_case e traz organization_id (a sua organização) e livemode (false no Sandbox, true no Live). Ignore os campos que você não usa; novos campos podem ser adicionados.

Payloads

invoice.created

CampoDescrição
invoice_idA fatura.
statusPENDING, ou AWAITING_SELECTION quando o comprador escolhe a rede.
livemodefalse no Sandbox (mode: "test" na API), true no Live.
network, tokenO par, ou null enquanto AWAITING_SELECTION.
amount_baseO preço em USD.
amount_totalO que o comprador precisa enviar, ou null enquanto AWAITING_SELECTION.
merchant_order_idComo enviado na fatura, ou null.
payment_link_idO payment link de onde a fatura veio, ou null.

invoice.detected, invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired

CampoDescrição
invoice_idA fatura.
statusO status da fatura depois da mudança.
livemodefalse no Sandbox, true no Live.
merchant_order_idComo enviado na fatura, ou null.
payment_link_idO payment link de onde a fatura veio, ou null.
network, tokenO par. null para uma fatura que expirou antes de o comprador escolher um.
amount_totalO que o comprador precisava enviar.
amount_receivedO que a fatura recebeu no seu token.

Associe o evento ao seu pedido com merchant_order_id, ou com o id da fatura que você guardou ao criá-la (invoice_id). Os eventos de fatura não trazem metadata: se precisar, leia a fatura com GET /api/v1/crypto/invoices/{id}.

payout.completed, payout.failed

{
  "organization_id": "cm2k8x1qv0000l708h3d9f2ab",
  "payout_id": "po_01JB7SA2K9D4F6H8J0L2N4P6RT",
  "status": "COMPLETED",
  "livemode": false,
  "account": "halden_type",
  "network": "POLYGON_AMOY",
  "token": "USDC",
  "amount": "50.00",
  "fee": "0.10",
  "net_amount": "49.90",
  "destination_address": "0x7a3B9c1D5e2F4a6B8c0D1e3F5a7B9c2D4e6F8a0B",
  "tx_hash": "0x5c8e2f1a9b7d3c6e4f0a2b8d1c9e7f3a5b6c4d2e0f1a3b5c7d9e2f4a6b8c0d1e"
}

account é a referência da conta que recebeu o payout. payout.failed acrescenta failure_reason.

refund.claimable

CampoDescrição
refund_id, invoice_idO reembolso e a fatura a que ele pertence.
statusCLAIMABLE.
reasonOVERPAYMENT, EXPIRED_UNDERPAID, LATE_PAYMENT ou WRONG_TOKEN.
livemode, network, tokenOnde o reembolso será pago, neste token.
amountO valor retido, antes da taxa de rede.
claim_urlO link privado de resgate do comprador. Encaminhe-o ao comprador; não o publique.
buyer_emailO buyer_email da fatura. Quando definido, o Aliquot já enviou o link por e-mail.
merchant_order_idO merchant_order_id da fatura, ou null.

refund.completed

refund_id, invoice_id, status (COMPLETED), reason, livemode, network, token, gross_amount, network_fee, net_amount (o que o comprador recebeu), destination_address e tx_hash.

Verificar a assinatura

A assinatura é um HMAC-SHA256, com o segredo inteiro do endpoint como chave (incluindo o prefixo whsec_), sobre a string <t>.<raw body>, em que t é o timestamp do header e o corpo bruto são exatamente os bytes que você recebeu. Calcule-a sobre o corpo bruto antes de fazer o parse do JSON: reserializar um objeto já parseado pode alterar os bytes.

import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * rawBody: o corpo da requisição como foi recebido (string).
 * header: o header X-Webhook-Signature.
 * secret: o segredo do endpoint, "whsec_…".
 */
export function verifySignature(rawBody, header, secret, toleranceSeconds = 300) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '');
  if (!match) return false;
  const [, t, v1] = match;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
}

Com Express, leia o corpo com express.raw({ type: 'application/json' }) e passe req.body.toString('utf8'). O Início rápido tem um receptor completo.

Cada tentativa é assinada com o horário em que é enviada, então uma nova tentativa traz um t novo. Rejeitar timestamps com mais de alguns minutos protege você de requisições reenviadas por terceiros sem rejeitar as novas tentativas.

Respostas e novas tentativas

  • Responda com qualquer status 2xx em até 10 segundos. Qualquer outra resposta, um timeout ou um erro de conexão conta como falha.
  • Redirecionamentos não são seguidos: um 3xx é uma falha.
  • Uma entrega que falha é repetida com intervalos cada vez maiores, até 7 tentativas no total: cada nova tentativa vem 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas e depois 24 horas após a tentativa anterior. A última tentativa acontece cerca de 32 horas e meia depois da primeira; se ela também falhar, a entrega fica como falha. Você ainda pode reenviá-la.
  • Cada evento é uma entrega separada, e as novas tentativas podem se intercalar, então os eventos podem chegar fora de ordem. Decida com base em data.status, ou lendo o objeto pela API, e não pela ordem de chegada.
  • Processe cada id uma única vez: o mesmo evento pode chegar mais de uma vez.

As novas tentativas que vencem enquanto o endpoint está desativado contam como tentativas que falharam. Não dependa apenas dos webhooks: se o seu endpoint ficar fora do ar por mais tempo que o calendário de novas tentativas, ou se um evento se perder do seu lado, reconcilie pela API. GET /api/v1/crypto/invoices?status=PAID lista as faturas pagas, e GET /api/v1/crypto/invoices/{id} devolve o estado atual de uma delas.

Reenviar uma entrega

Toda tentativa fica registrada. Desenvolvedores → Webhooks no dashboard as lista com o status HTTP de cada uma e uma ação Reenviar. Reenviar manda o mesmo evento para o mesmo endpoint de novo: mesmo id, type, occurredAt e data, assinado com o segredo atual do endpoint e um timestamp novo. É uma única tentativa, sem o calendário de novas tentativas, e aparece na lista como uma nova entrega. Como o id é o mesmo, um receptor que já processou o evento o ignora.

O equivalente na API é POST …/webhooks/deliveries/{id}/resend (veja abaixo), que responde 202 quando a tentativa entra na fila. Responde 409 CONFLICT se o endpoint estiver desativado, ou para uma tentativa registrada antes de os reenvios existirem.

Gerenciar endpoints

Adicione endpoints no dashboard em Desenvolvedores → Webhooks: a URL do endpoint e os eventos que ele recebe. invoice.paid, invoice.underpaid e refund.claimable vêm pré-selecionados. A URL precisa usar https:// e resolver para um endereço público. O segredo de assinatura (whsec_ seguido de 64 caracteres hexadecimais) é exibido uma única vez, quando o endpoint é criado.

Um endpoint recebe apenas os eventos que lista, e apenas enquanto está ativado. Cada evento é entregue a todos os endpoints ativados que o assinam.

Na mesma página, cada endpoint tem Editar (URL, eventos, ativado ou desativado), Rotacionar segredo e Excluir. Excluir um endpoint apaga o histórico de entregas e descarta as novas tentativas pendentes.

As mesmas operações estão disponíveis como endpoints da API sob /api/v1/organizations/{organizationId}/webhooks. Elas aceitam uma sessão do dashboard ou uma API key, e exigem a permissão webhooks:manage (veja Autenticação):

EndpointDescrição
GET …/webhooks/endpointsLista os endpoints.
POST …/webhooks/endpointsCria um endpoint. Corpo { "url", "events", "enabled" }. A resposta inclui secret, uma única vez.
PATCH …/webhooks/endpoints/{id}Altera url, events ou enabled.
DELETE …/webhooks/endpoints/{id}Exclui um endpoint.
POST …/webhooks/endpoints/{id}/rotate-secretSubstitui o segredo de assinatura. A resposta inclui o novo secret, uma única vez.
GET …/webhooks/deliveriesTentativas de entrega, das mais recentes para as mais antigas. Filtros: endpointId, status (pending, success, failed), page, pageSize.
POST …/webhooks/deliveries/{id}/resendEnvia de novo o evento dessa tentativa, uma única tentativa. 202, sem corpo.

Um objeto endpoint tem id, organizationId, url, events, enabled, lastDeliveryAt, lastDeliveryStatus, createdAt e updatedAt. Uma tentativa de entrega tem id, endpointId, eventType, payloadHash (SHA-256 do corpo enviado), status, httpStatus, attemptCount, deliveredAt e createdAt. Esses objetos de gerenciamento usam camelCase, ao contrário da API de pagamentos e dos payloads dos webhooks.

Rotacionar o segredo

Rotacionar substitui o segredo imediatamente: a partir desse momento, toda entrega, incluindo as novas tentativas de eventos anteriores, é assinada com o novo segredo, e o antigo deixa de verificar. Para rotacionar sem rejeitar entregas:

  1. Faça o deploy do seu receptor de forma que ele aceite uma assinatura que verifique com o segredo antigo ou com o novo.
  2. Rotacione: Rotacionar segredo no endpoint, no dashboard, ou POST …/webhooks/endpoints/{id}/rotate-secret. Guarde o novo secret, exibido uma única vez, como o novo.
  3. Quando as entregas verificarem com o novo segredo, remova o antigo do seu receptor.