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.
Eventos
Sección titulada «Eventos»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.
El campo antiguo event
Sección titulada «El campo antiguo event»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.
Use el helper del SDK si puede
Sección titulada «Use el helper del SDK si puede»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.
Verificarla usted mismo
Sección titulada «Verificarla usted mismo»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.
Gestionar las entregas
Sección titulada «Gestionar las entregas»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.
