Ir al contenido

Webhooks

Configure callbackUrl en un pago y Stablezact enviará un POST a esa URL a medida que el pago avanza. Esta es la forma fiable de saber que un pago se ha liquidado, porque no depende de que el cliente mantenga abierta una pestaña del navegador.

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

Tanto orderId como metadata vuelven en cada evento, y así puede asociar un evento a un pedido sin mantener su propia tabla de correspondencias.

Lea type. Es el mismo vocabulario reducido sin importar cómo haya pagado el cliente.

type Significado Completar pedido
payment.pending se ha detectado dinero, el pago todavía no es definitivo no
payment.succeeded el cliente ha pagado sí
payment.underpaid ha llegado menos de lo facturado no, avise al cliente
payment.failed caducado, cancelado o fallido no
payment.refunded el dinero se ha devuelto al cliente revierta el pedido
settlement.completed su liquidación ha llegado a su wallet ya completado
settlement.delayed su liquidación va más lenta de lo habitual, se reintenta por sí sola nada que hacer
settlement.failed su liquidación no se pudo completar, nos pondremos en contacto con usted nada que hacer
webhook.test una entrega de prueba no

payment.succeeded es el único que significa completar el pedido. Todos los demás están ahí por si los quiere.

Los eventos payment responden a «¿debo enviar este pedido?». Los eventos settlement responden a «¿dónde está mi dinero?», y nunca cambian si debe enviarlo o no. Muchas integraciones los ignoran y concilian a partir de su wallet.

type también se envía en la cabecera X-Webhook-Type, por si prefiere enrutar según una cabecera en lugar de analizar el cuerpo.

Cada payload también incluye un campo event, anterior a type. Usa dos convenciones de nombres, da dos nombres distintos al mismo resultado según cómo haya pagado el cliente y nombra nuestros pasos internos de liquidación. Sigue funcionando y seguirá funcionando, así que las integraciones existentes no necesitan cambios.

Construya las integraciones nuevas sobre type. Si está leyendo una antigua, esta es la correspondencia entre ambos:

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

El pago es paymentId en todos los payloads. Algunos eventos antiguos también lo llaman transactionId; ambos están presentes y son idénticos. La transacción de liquidación es settlementTxHash, que en los eventos antiguos también aparece como sweepTxHash.

Si no está seguro de qué evento está viendo, obtenga el pago y lea outcome. Es la misma respuesta, y no depende de qué eventos le hayan llegado.

Verificar que la solicitud procede de nosotros

Sección titulada «Verificar que la solicitud procede de nosotros»

Cada entrega incluye dos cabeceras:

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

Verifíquelas antes de actuar sobre el cuerpo. Un endpoint que completa pedidos con un POST no verificado completará pedidos para cualquiera que encuentre la URL.

El SDK incluye la verificación, así que no tiene que volver a implementarla:

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);
});

También existe coinleyWebhookMiddleware, por si prefiere montarlo 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);
}

Tres detalles importantes:

El timestamp forma parte del material firmado, así que hay que añadirlo al payload antes de serializar, como número.

Las claves se ordenan. JSON.stringify(value, Object.keys(value).sort()) es exactamente la llamada que hace el servidor.

Su secreto de webhook debe tener al menos 32 caracteres. El verificador rechaza los secretos más cortos en lugar de debilitar la firma sin avisar.

Responda 200 rápidamente y haga el trabajo después. Un endpoint lento parece un endpoint que ha fallado.

Prepárese para recibir el mismo evento dos veces. Los reintentos y los timeouts de red implican una entrega al menos una vez, así que usar paymentId más event como clave e ignorar las repeticiones compensa las pocas líneas que cuesta.