Webhook
A Bitpayway envia um webhook quando uma ordem de pagamento muda para um dos seguintes status:
partial paidpaidexpired partial paidexpiredcanceled
Nenhum webhook é enviado para o status inicial pending na criação da ordem, nem enquanto um pagamento que completa a ordem aguarda as confirmações de bloco (paid waiting confirmations): você é notificado quando a ordem fica paid. A única exceção são as ordens multidestino: uma ordem multidestino cujo settlementStatus mudou para mismatch é notificada mesmo que o status ainda seja pending ou paid waiting confirmations.
Para evitar uma requisição HTTP separada para cada ordem processada, as notificações são agrupadas por negócio antes do envio, com até 30 notificações em cada requisição. Por isso, o corpo da requisição é sempre um array JSON, mesmo quando há apenas uma notificação.
Status de uma ordem
A tabela abaixo lista todos os status que uma ordem pode ter, inclusive os que não disparam webhook. Eles podem aparecer ao consultar uma ordem pela API e no campo status de uma notificação.
| Status | Significado |
|---|---|
pending | Aguardando pagamento. |
partial paid | Parte do valor foi paga e confirmada. |
paid waiting confirmations | O valor foi pago e aguarda as confirmações de bloco. |
paid | O valor foi pago e confirmado. |
expired | Os 30 minutos terminaram sem pagamento. |
expired partial paid | Os 30 minutos terminaram com só parte do valor paga. |
canceled | A ordem foi cancelada. |
Uma ordem aceita pagamentos por 30 minutos depois de criada. Uma ordem que não foi paga por completo nesse prazo fica expired, ou expired partial paid se recebeu parte do valor.
Requisição HTTP
Método: POST
Headers:
Content-Type: application/jsonSPP-Api-Timestamp: timestamp Unix em milissegundos que indica quando a requisição foi enviada.SPP-Api-Signature: assinatura HMAC-SHA256 no formatosha256=<digest hexadecimal>.
Body:
O corpo é um array de notificações. Cada notificação tem notification: 'order' e um objeto data com a ordem. O formato de data depende do modelo de pagamento da ordem:
- Ordem tradicional:
datacontém apenas os campos comuns. O campopaymentModelnão está presente. - Ordem multidestino:
datacontém os campos comuns mais os campos multidestino, epaymentModelé'split'.
Use a presença de paymentModel === 'split' para distinguir os dois casos. Uma integração que não usa pagamentos multidestino pode ignorar os campos adicionais.
type WebhookBody = OrderNotification[];
type OrderNotification = {
notification: 'order';
data: StandardOrderData | MultiDestinationOrderData;
};
// Campos presentes em toda notificação.
type StandardOrderData = {
id: string;
requestId: string;
description: string;
amount: string;
status: OrderStatus;
network: string;
currency: string;
paidAmount: string;
netAmount: string;
fee: string;
};
// Ordem multidestino: todos os campos comuns + os campos abaixo.
type MultiDestinationOrderData = StandardOrderData & {
paymentModel: 'split';
settlementStatus: 'pending' | 'matched' | 'mismatch';
settlementVersion: number;
split: {
partnerId: string;
partnerCodeId: string;
partnerAmount: string;
};
partnerNetAmount: string;
partnerGrossAmount: string;
clientCommission: string;
partnerCommission: string;
commissionReceived: string;
};
type OrderStatus =
| 'pending' // apenas em ordens multidestino em `mismatch`
| 'paid waiting confirmations' // apenas em ordens multidestino em `mismatch`
| 'partial paid'
| 'paid'
| 'expired partial paid'
| 'expired'
| 'canceled';
Campos comuns
Presentes em todas as notificações, de ordens tradicionais e multidestino.
| Campo | Descrição |
|---|---|
id | UUID gerado pela Bitpayway para a ordem de pagamento. |
requestId | UUID informado por sua aplicação ao criar a ordem. |
description | Descrição informada na criação da ordem. |
amount | Valor solicitado para o pagamento, representado como string decimal. |
status | Status atual da ordem detectado pela Bitpayway. |
network | Nome da rede blockchain. |
currency | Símbolo do token. |
paidAmount | Valor total pago na blockchain antes das taxas, representado como string decimal. |
netAmount | Valor enviado para a sua carteira após as taxas de pagamento, representado como string decimal. |
fee | Spread da plataforma, representado como string decimal. Em ordens multidestino, é a soma de clientCommission e partnerCommission. |
Todos os valores monetários são strings decimais já convertidas para a unidade do token (por exemplo, "1000.52" USDC), não valores brutos em wei.
Exemplo: ordem tradicional
[
{
"notification": "order",
"data": {
"id": "45ccdc4c-8457-4e9c-a16d-4c9d35f128ed",
"requestId": "2f3c9d2c-1b75-4e9d-9b2f-67d0a9c3c7d1",
"description": "Pedido #1234",
"amount": "1000.52",
"status": "partial paid",
"network": "ethereum",
"currency": "USDC",
"paidAmount": "1000",
"netAmount": "950",
"fee": "50"
}
}
]
Ordens multidestino
Uma ordem multidestino envia a mesma notificação, acrescida dos campos abaixo. Eles são ausentes nas ordens tradicionais, então uma integração existente continua funcionando sem alteração.
| Campo | Descrição |
|---|---|
paymentModel | Sempre split. Ausente nas ordens tradicionais. |
settlementStatus | pending, matched ou mismatch. Uma ordem em mismatch exige conciliação com o parceiro. É independente do status da ordem. |
settlementVersion | Versão dos valores confirmados; aumenta sempre que eles mudam. |
split.partnerId | UUID do vínculo do parceiro. |
split.partnerCodeId | Identificador on-chain do vínculo do parceiro. |
split.partnerAmount | Parcela bruta acordada na criação da ordem. |
netAmount | Valor efetivamente recebido por você. |
partnerNetAmount | Valor efetivamente recebido pelo parceiro. |
partnerGrossAmount | Valor bruto acumulado destinado ao parceiro. |
clientCommission | Comissão nominal sobre a sua parcela. |
partnerCommission | Comissão nominal sobre a parcela do parceiro. |
commissionReceived | Valor efetivamente recebido pela plataforma. |
Exemplo: ordem multidestino
[
{
"notification": "order",
"data": {
"id": "45ccdc4c-8457-4e9c-a16d-4c9d35f128ed",
"requestId": "2f3c9d2c-1b75-4e9d-9b2f-67d0a9c3c7d1",
"description": "Pedido #1234",
"amount": "1000",
"status": "paid",
"network": "ethereum",
"currency": "USDC",
"paidAmount": "1000",
"netAmount": "665",
"fee": "50",
"paymentModel": "split",
"settlementStatus": "matched",
"settlementVersion": 1,
"split": {
"partnerId": "9b1d2c3e-4f50-4a6b-8c7d-0e1f2a3b4c5d",
"partnerCodeId": "12",
"partnerAmount": "300"
},
"partnerNetAmount": "285",
"partnerGrossAmount": "300",
"clientCommission": "35",
"partnerCommission": "15",
"commissionReceived": "50"
}
}
]
Quando uma ordem multidestino é notificada
Além das mudanças de status, uma ordem multidestino também é notificada sempre que o settlementStatus ou os valores confirmados mudam — o que incrementa o settlementVersion. Isso tem duas consequências:
- Uma notificação pode chegar depois de um status terminal, então uma ordem multidestino pode ser notificada mais de uma vez e o
statuspode se repetir. - Uma ordem em
settlementStatus: "mismatch"é notificada mesmo comstatus: "pending", para que você possa iniciar a conciliação antes de qualquer confirmação de pagamento.
Considere o settlementVersion parte da sua chave de idempotência para as atualizações financeiras e não repita a entrega comercial a cada atualização. Uma resposta a uma notificação antiga não confirma a entrega de uma versão mais recente.
Veja Pagamentos multidestino para entender como os valores são calculados e quando uma ordem é considerada aprovada.
Entrega e novas tentativas
Sua aplicação deve processar todos os itens do array e retornar uma resposta 2xx após aceitar as notificações.
O resultado de uma requisição HTTP se aplica ao lote completo:
- Uma resposta
2xxbem-sucedida marca como notificadas todas as ordens cujo status não mudou desde a criação do lote. - Um timeout, erro de conexão ou resposta diferente de
2xxmantém o lote completo elegível para uma nova tentativa.
A Bitpayway realiza até cinco tentativas de entrega. Como uma requisição pode ser processada pelo seu endpoint sem que a Bitpayway receba uma resposta bem-sucedida, uma nova tentativa pode entregar notificações duplicadas. Processe as notificações de forma idempotente, utilizando em conjunto o id e o status da ordem como chave de idempotência.
Verificação da assinatura
A assinatura é calculada a partir do corpo bruto exato da requisição:
<timestamp>\n<método HTTP em maiúsculas>\n<corpo bruto da requisição>
No webhook, o método HTTP é POST. A URL e o path não fazem parte do conteúdo assinado.
Utilize como segredo HMAC a chave secreta de webhook gerada nas configurações do negócio. Capture o corpo bruto antes de interpretá-lo como JSON; serializar novamente o JSON já processado pode produzir bytes diferentes e invalidar a assinatura.
O exemplo abaixo verifica a assinatura utilizando Node.js:
import crypto from 'node:crypto';
const verifyHmacSignature = (params: {
apiSecret: string;
timestampHeader: string | undefined;
signatureHeader: string | undefined;
method: string;
rawBody: string;
maxSkewMs?: number;
}) => {
const {
apiSecret,
timestampHeader,
signatureHeader,
method,
rawBody,
maxSkewMs = 1000 * 60 * 5, // 5 minutos
} = params;
if (!timestampHeader || !signatureHeader) {
return false;
}
const timestamp = Number(timestampHeader);
if (!Number.isFinite(timestamp)) {
return false;
}
// Proteção contra ataques de repetição.
if (Math.abs(Date.now() - timestamp) > maxSkewMs) {
return false;
}
const expectedPrefix = 'sha256=';
if (!signatureHeader.startsWith(expectedPrefix)) {
return false;
}
const receivedHmacHex = signatureHeader.slice(expectedPrefix.length);
const stringToSign = [timestamp.toString(), method.toUpperCase(), rawBody].join('\n');
const calculatedHmacHex = crypto
.createHmac('sha256', apiSecret)
.update(stringToSign, 'utf8')
.digest('hex');
const receivedBuffer = Buffer.from(receivedHmacHex, 'hex');
const calculatedBuffer = Buffer.from(calculatedHmacHex, 'hex');
if (receivedBuffer.length !== calculatedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuffer, calculatedBuffer);
};
A tolerância de cinco minutos deste exemplo é uma política do cliente e pode ser ajustada. É altamente recomendável validar o timestamp para reduzir o risco de ataques de repetição.