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
| Evento | Enviado quando |
|---|---|
invoice.created | Uma fatura é criada, pela API ou a partir de um payment link. |
invoice.detected | A 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.paid | O 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.overpaid | Enviado junto com invoice.paid quando o comprador enviou mais do que o total. O excedente vira um reembolso. |
invoice.underpaid | O valor confirmado está abaixo do total. O comprador ainda pode enviar o restante antes de a janela terminar. |
invoice.expired | A janela de pagamento terminou: status é EXPIRED (nada recebido) ou EXPIRED_UNDERPAID (um pagamento parcial, retido como reembolso). |
payout.completed | Uma transação de payout atingiu o limiar de confirmações. |
payout.failed | Uma transação de payout foi revertida ou descartada. O valor volta ao saldo disponível. |
refund.claimable | Há dinheiro retido para um comprador que pode ser resgatado. Traz claim_url. |
refund.completed | Um 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:
| Header | Valor |
|---|---|
X-Webhook-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-Webhook-Timestamp | O mesmo t, sozinho. |
X-Webhook-Event | O 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"
}
}| Campo | Descrição |
|---|---|
id | Identifica o evento. É o mesmo em todas as novas tentativas do mesmo evento, então use-o para processar cada evento uma única vez. |
type | O nome do evento. |
occurredAt | Quando a mudança aconteceu (não quando a entrega foi enviada). |
data | O 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
| Campo | Descrição |
|---|---|
invoice_id | A fatura. |
status | PENDING, ou AWAITING_SELECTION quando o comprador escolhe a rede. |
livemode | false no Sandbox (mode: "test" na API), true no Live. |
network, token | O par, ou null enquanto AWAITING_SELECTION. |
amount_base | O preço em USD. |
amount_total | O que o comprador precisa enviar, ou null enquanto AWAITING_SELECTION. |
merchant_order_id | Como enviado na fatura, ou null. |
payment_link_id | O payment link de onde a fatura veio, ou null. |
invoice.detected, invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired
| Campo | Descrição |
|---|---|
invoice_id | A fatura. |
status | O status da fatura depois da mudança. |
livemode | false no Sandbox, true no Live. |
merchant_order_id | Como enviado na fatura, ou null. |
payment_link_id | O payment link de onde a fatura veio, ou null. |
network, token | O par. null para uma fatura que expirou antes de o comprador escolher um. |
amount_total | O que o comprador precisava enviar. |
amount_received | O 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
| Campo | Descrição |
|---|---|
refund_id, invoice_id | O reembolso e a fatura a que ele pertence. |
status | CLAIMABLE. |
reason | OVERPAYMENT, EXPIRED_UNDERPAID, LATE_PAYMENT ou WRONG_TOKEN. |
livemode, network, token | Onde o reembolso será pago, neste token. |
amount | O valor retido, antes da taxa de rede. |
claim_url | O link privado de resgate do comprador. Encaminhe-o ao comprador; não o publique. |
buyer_email | O buyer_email da fatura. Quando definido, o Aliquot já enviou o link por e-mail. |
merchant_order_id | O 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
2xxem 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
iduma ú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):
| Endpoint | Descrição |
|---|---|
GET …/webhooks/endpoints | Lista os endpoints. |
POST …/webhooks/endpoints | Cria 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-secret | Substitui o segredo de assinatura. A resposta inclui o novo secret, uma única vez. |
GET …/webhooks/deliveries | Tentativas de entrega, das mais recentes para as mais antigas. Filtros: endpointId, status (pending, success, failed), page, pageSize. |
POST …/webhooks/deliveries/{id}/resend | Envia 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:
- Faça o deploy do seu receptor de forma que ele aceite uma assinatura que verifique com o segredo antigo ou com o novo.
- Rotacione: Rotacionar segredo no endpoint, no dashboard, ou
POST …/webhooks/endpoints/{id}/rotate-secret. Guarde o novosecret, exibido uma única vez, como o novo. - Quando as entregas verificarem com o novo segredo, remova o antigo do seu receptor.