Zum Inhalt springen

Webhooks

Setzen Sie callbackUrl für eine Zahlung, und Stablezact sendet POST-Anfragen an diese URL, während die Zahlung fortschreitet. Das ist der zuverlässige Weg zu erfahren, dass eine Zahlung abgewickelt ist, weil er nicht davon abhängt, dass der Kunde einen Browser-Tab offen lässt.

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

Sowohl orderId als auch metadata werden bei jedem Ereignis zurückgegeben. So ordnen Sie ein Ereignis einer Bestellung zu, ohne eine eigene Zuordnung zu pflegen.

Lesen Sie type. Es ist dasselbe kleine Vokabular, egal wie der Kunde bezahlt hat.

type Bedeutung Ausführen
payment.pending Geld wurde erkannt, die Zahlung ist noch nicht final nein
payment.succeeded der Kunde hat bezahlt ja
payment.underpaid weniger als der Rechnungsbetrag ist angekommen nein, Kunden darauf hinweisen
payment.failed abgelaufen, storniert oder fehlgeschlagen nein
payment.refunded das Geld ging an den Kunden zurück Bestellung rückgängig machen
settlement.completed Ihre Auszahlung hat Ihre Wallet erreicht bereits ausgeführt
settlement.delayed Ihre Auszahlung dauert länger als üblich, wird automatisch erneut versucht nichts zu tun
settlement.failed Ihre Auszahlung konnte nicht abgeschlossen werden, wir kontaktieren Sie nichts zu tun
webhook.test eine Testzustellung nein

payment.succeeded ist das einzige Ereignis, das „ausführen“ bedeutet. Alle anderen sind da, falls Sie sie brauchen.

Die payment-Ereignisse beantworten die Frage „Soll ich diese Bestellung versenden?“. Die settlement-Ereignisse beantworten „Wo ist mein Geld?“, und sie ändern nie etwas daran, ob Sie versenden. Viele Integrationen ignorieren sie und gleichen stattdessen über ihre Wallet ab.

type wird außerdem als Header X-Webhook-Type gesendet, falls Sie lieber anhand eines Headers routen, als den Body zu parsen.

Jede Payload enthält außerdem ein Feld event, das älter ist als type. Es verwendet zwei Namenskonventionen, gibt demselben Ergebnis je nach Zahlungsweise des Kunden zwei verschiedene Namen und benennt unsere internen Auszahlungsschritte. Es funktioniert weiterhin und wird auch weiterhin funktionieren, bestehende Integrationen müssen also nicht geändert werden.

Bauen Sie neue Integrationen auf type auf. Wenn Sie eine ältere lesen, so entsprechen sich die beiden:

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

Die Zahlung ist in jeder Payload paymentId. Manche älteren Ereignisse nennen sie außerdem transactionId; beide sind vorhanden und identisch. Die Auszahlungstransaktion ist settlementTxHash, bei älteren Ereignissen auch als sweepTxHash vorhanden.

Wenn Sie nicht sicher sind, welches Ereignis Sie vor sich haben, rufen Sie die Zahlung ab und lesen Sie outcome. Es ist dieselbe Antwort, und sie hängt nicht davon ab, welche Ereignisse Sie erreicht haben.

Jede Zustellung enthält zwei Header:

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

Verifizieren Sie, bevor Sie auf den Body reagieren. Ein Endpunkt, der Bestellungen auf einen unverifizierten POST hin ausführt, führt Bestellungen für jeden aus, der die URL findet.

Das SDK liefert die Verifizierung mit, sodass Sie sie nicht neu implementieren müssen:

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

Es gibt außerdem coinleyWebhookMiddleware, falls Sie die Verifizierung lieber als Middleware einbinden möchten.

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

Drei Details, auf die es ankommt:

Der timestamp ist Teil des signierten Materials und muss daher vor dem Serialisieren als Zahl zur Payload hinzugefügt werden.

Die Schlüssel werden sortiert. JSON.stringify(value, Object.keys(value).sort()) ist genau der Aufruf, den der Server ausführt.

Ihr Webhook-Secret muss mindestens 32 Zeichen lang sein. Kürzere Secrets werden vom Verifizierer abgelehnt, statt die Signatur stillschweigend zu schwächen.

Antworten Sie schnell mit 200 und erledigen Sie die Arbeit danach. Ein langsamer Endpunkt sieht aus wie ein fehlgeschlagener.

Rechnen Sie damit, dass dasselbe Ereignis zweimal ankommt. Wiederholungen und Netzwerk-Timeouts bedeuten eine Zustellung mindestens einmal (at-least-once). Daher lohnen sich die paar Zeilen, um anhand von paymentId plus event zu deduplizieren und Wiederholungen zu ignorieren.