Pular para o conteúdo

Webhooks

Defina callbackUrl num pagamento e a Stablezact fará POST para esse endereço à medida que o pagamento avança. É a forma fiável de saber que um pagamento foi liquidado, porque não depende de o cliente manter um separador do navegador aberto.

config={{
amount: 49.99,
currency: 'USDC',
callbackUrl: 'https://yourstore.com/webhooks/stablezact',
orderId: 'ORD-1234',
metadata: { basket: 'b-99' },
}}

Tanto orderId como metadata são devolvidos em cada evento, e é assim que associa um evento a uma encomenda sem manter o seu próprio mapeamento.

Leia type. É o mesmo vocabulário reduzido, seja qual for a forma como o cliente pagou.

type Significado Processar a encomenda
payment.pending o dinheiro foi detetado, o pagamento ainda não é final não
payment.succeeded o cliente pagou sim
payment.underpaid chegou menos do que o valor faturado não, avise o cliente
payment.failed expirado, cancelado ou falhado não
payment.refunded o dinheiro foi devolvido ao cliente anule a encomenda
settlement.completed a transferência chegou à sua wallet já processada
settlement.delayed a transferência está mais lenta do que o habitual, com novas tentativas automáticas nada a fazer
settlement.failed não foi possível concluir a transferência, entramos em contacto consigo nada a fazer
webhook.test uma entrega de teste não

payment.succeeded é o único que significa processar a encomenda. Todos os outros estão disponíveis, se os quiser usar.

Os eventos payment respondem a “devo enviar esta encomenda”. Os eventos settlement respondem a “onde está o meu dinheiro” e nunca alteram a decisão de enviar. Muitas integrações ignoram-nos e fazem a reconciliação a partir da sua wallet.

type também é enviado no cabeçalho X-Webhook-Type, se preferir encaminhar com base num cabeçalho em vez de analisar o corpo.

Cada payload inclui também um campo event, anterior a type. Usa duas convenções de nomes, dá dois nomes diferentes ao mesmo resultado consoante a forma como o cliente pagou e expõe os nossos passos internos de liquidação. Continua a funcionar e vai continuar a funcionar, por isso as integrações existentes não precisam de ser alteradas.

Construa as novas integrações com base em type. Se estiver a ler uma integração antiga, esta é a correspondência entre os dois:

event type
payment_success, deposit.confirmed payment.succeeded
payment_failure, deposit.failed, bridge.failed payment.failed
deposit.detected, bridge.filled payment.pending
deposit.swept, settlement.completed settlement.completed
settlement.delayed settlement.delayed
payment.underpaid payment.underpaid
payment.refunded payment.refunded
webhook_test webhook.test

O pagamento é paymentId em todos os payloads. Alguns eventos antigos também lhe chamam transactionId; ambos estão presentes e são idênticos. A transação da transferência é settlementTxHash, também presente como sweepTxHash nos eventos antigos.

Se não tiver a certeza de que evento está a ver, obtenha o pagamento e leia outcome. É a mesma resposta, e não depende de quais eventos lhe chegaram.

Cada entrega inclui dois cabeçalhos:

X-Webhook-Signature: sha256=<hex digest>
X-Webhook-Timestamp: <milliseconds since epoch>

Verifique antes de agir com base no corpo. Um endpoint que processa encomendas com base num POST não verificado processa encomendas para qualquer pessoa que descubra o URL.

O SDK inclui a verificação, por isso não tem de a reimplementar:

import { verifyWebhookSignature } from 'stablezact-pay';
app.post('/webhooks/stablezact', express.json(), (req, res) => {
const result = verifyWebhookSignature(
req.body,
req.headers['x-webhook-signature'],
req.headers['x-webhook-timestamp'],
process.env.STABLEZACT_WEBHOOK_SECRET
);
if (!result.valid) return res.status(400).json({ error: result.error });
res.sendStatus(200);
handleEvent(req.body);
});

Existe também coinleyWebhookMiddleware, se preferir montá-lo como middleware.

import crypto from 'node:crypto';
function verify(payload, signatureHeader, timestampHeader, secret) {
const ts = parseInt(timestampHeader, 10);
// Reject anything older than five minutes, so a captured delivery cannot be
// replayed later.
if (Date.now() - ts > 5 * 60 * 1000) return false;
const withTimestamp = { ...payload, timestamp: ts };
const serialized = JSON.stringify(withTimestamp, Object.keys(withTimestamp).sort());
const expected = crypto.createHmac('sha256', secret).update(serialized).digest('hex');
const received = signatureHeader.replace(/^sha256=/, '');
const a = Buffer.from(received, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Três detalhes importantes:

O timestamp faz parte do material assinado, por isso tem de ser adicionado ao payload antes da serialização, como número.

As chaves são ordenadas. JSON.stringify(value, Object.keys(value).sort()) é exatamente a chamada que o servidor faz.

O seu segredo de webhook tem de ter pelo menos 32 caracteres. Segredos mais curtos são rejeitados pelo verificador, em vez de enfraquecerem a assinatura sem aviso.

Responda 200 rapidamente e faça o trabalho depois. Um endpoint lento parece um endpoint que falhou.

Trate o caso de o mesmo evento chegar duas vezes. As novas tentativas e os timeouts de rede significam entrega pelo menos uma vez (at-least-once), por isso usar paymentId mais event como chave e ignorar as repetições compensa as poucas linhas que exige.