Was macht Nomba Webhooks?
Ein Webhook ist die Art und Weise, wie Nomba deinem Server mitteilt, dass Geld tatsächlich bewegt wurde. Jeder im Internet kann eine POST-Anfrage an deine Webhook-URL senden, daher muss der Handler nachweisen, dass jede Anfrage tatsächlich von Nomba stammt, bevor er eine Bestellung als bezahlt markiert oder eine Rückerstattung vornimmt. Außerdem muss er mit wiederholten Zustellungen desselben Ereignisses durch Nomba umgehen können.
Verwende den offiziellen SDK-Verifier; implementiere das HMAC nicht selbst. Nombas Signatur deckt bestimmte Felder ab, die mit : verbunden sind, und die SDKs verhindern zudem einen Replay-Angriff, den eine naive Implementierung übersehen würde.
| Sprache | Verifikation |
|---|---|
Node.js / TS (@nombaoss/nomba-node) | verifyWebhook(rawBody, headers, signingKey) |
Python (nomba) | from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key) |
PHP (nombaoss/nomba-php) | Nomba\Webhook::verify($rawBody, $headers, $signingKey) |
Genaue Importe und Argumenttypen für jede Sprache findest du in references/sdk-setup.md. Jede Methode gibt das geparste Ereignis zurück oder wirft eine Ausnahme (WebhookVerificationError / WebhookVerificationException). Keine von ihnen benötigt API-Anmeldeinformationen, sondern nur den Signaturschlüssel aus dem Nomba-Dashboard (Entwickler → Webhook-Setup). Lies ihn aus einer Umgebungsvariable wie NOMBA_WEBHOOK_SIGNING_KEY. Es handelt sich um ein anderes Geheimnis als den API-Client-Secret.
Der Handler, Schritt für Schritt
- Den rohen Body abrufen. Verifiziere die genauen Bytes, die Nomba gesendet hat. Wenn ein JSON-Body-Parser zuerst ausgeführt wird und du das Objekt neu serialisierst, kann die Signatur nicht mehr übereinstimmen oder, schlimmer noch, du verifizierst etwas anderes als das, worauf du reagierst. Jedes Framework hat eine andere Möglichkeit, den rohen Body zu erhalten; siehe references/frameworks.md für die richtige Methode und schließe die Route vom CSRF-Schutz aus, wo das Framework einen solchen hat.
- Verifizieren, dann parsen. Verwende das vom Verifier zurückgegebene Ereignis. Bei Fehlschlag antworte mit
401(oder400) und stoppe. Protokolliere den vollständigen Body oder die Header abgelehnter Anfragen nicht auf Info-Ebene. - Deduplizieren anhand von
requestId. Nomba wiederholt Zustellungen, sodass dasselbe Ereignis mehrmals eintreffen kann. Zeichne jede verarbeiteterequestIdmit einer eindeutigen Einschränkung auf und überspringe bereits gesehene. Gib auch für Duplikate200zurück, sonst wiederholt Nomba die Zustellung weiter. - Schnell bestätigen. Antworte umgehend mit
200und erledige langsame Arbeiten (E-Mails, Fulfillment, Ledger-Updates) in einem Hintergrundjob, der mit derrequestIdverknüpft ist. Langsame Handler verursachen Timeouts und erneute Zustellungen. - Auf das Ereignis reagieren unter Verwendung der folgenden Tabelle. Ordne das Ereignis deinem eigenen Datensatz anhand der Referenz zu und prüfe, ob der Betrag und die Währung deinen Erwartungen entsprechen, bevor du die Bestellung erfüllst.
Was jedes Ereignis bedeutet
event_type | Abgleich mit | Aktion |
|---|---|---|
payment_success | Checkout: data.order.orderReference; Virtuelles Konto / Überweisung: data.transaction.aliasAccountReference oder transactionId | Als bezahlt markieren, nachdem data.transaction.transactionAmount (und Währung) gegen deine Bestellung geprüft wurde. Bei tokenisierten Checkouts data.tokenizedCardData.tokenKey (falls vorhanden und nicht "N/A") zusammen mit dem Kunden speichern, nur serverseitig. |
payment_failed | wie oben | Den fehlgeschlagenen Versuch markieren; den Kunden erneut versuchen lassen. Es wurde nichts abgebucht. |
payment_reversal | wie oben | Erhaltenes Geld wurde rückgängig gemacht. Fulfillment rückgängig machen oder zur Überprüfung markieren. |
payout_success | data.transaction.merchantTxRef | Die Überweisung oder Rechnungszahlung als zugestellt markieren. |
payout_failed | data.transaction.merchantTxRef | Noch nicht erstatten. Nomba kann es über einen anderen Weg erneut versuchen. Als fehlgeschlagen markieren und abwarten. |
payout_refund | data.transaction.merchantTxRef | Das Geld ist zurück in deiner Nomba-Wallet. Jetzt ist es sicher, deinem Kunden eine Rückerstattung zu geben oder gutzuschreiben und die Auszahlung als fehlgeschlagen zu markieren. |
Eine Rückerstattung bei payout_failed (oder bei einem Timeout) anstatt bei payout_refund ist der klassische Weg, wie Händler Kunden doppelt bezahlen. Bei jedem nicht erkannten Ereignis protokolliere es und gib 200 zurück.
Bestätige Geldentscheidungen mit der API. Nombas Signatur deckt nur event_type, requestId, data.merchant.userId, data.merchant.walletId, data.transaction.transactionId, type, time, responseCode und den Zeitstempel ab. Der Betrag, orderReference, merchantTxRef und der Karten-tokenKey sind nicht signiert. Bevor du also eine Bestellung erfüllst, eine Rückerstattung vornimmst oder einen Kartentoken speicherst, frage mit dem SDK erneut an und handle basierend auf der API-Antwort: transactions.get({ orderReference }) für Checkout-Zahlungen (auch checkout.getTransaction in der Produktion) und transactions.get({ merchantTxRef }) für Auszahlungen. Die Deduplizierung anhand der signierten requestId verhindert eine manipulierte Kopie eines bereits verarbeiteten Ereignisses, und die erneute Abfrage deckt den Rest ab.
Replay-Schutz
Die Deduplizierung anhand der requestId ist die Hauptverteidigung: Eine wiederholte oder erneut zugestellte Lieferung eines bereits verarbeiteten Ereignisses ändert nichts.
Die Verifier können auch alte Zeitstempel ablehnen (toleranceSeconds / tolerance_seconds / das vierte Argument in PHP), aber lass dies standardmäßig deaktiviert. Nomba dokumentiert nomba-timestamp als die Zeit, zu der die Nutzlast gesendet wurde, ohne zu sagen, ob automatische Wiederholungen einen neuen Zeitstempel erhalten. Wenn sie den ursprünglichen behalten, würde eine Toleranz, die kürzer ist als Nombas Wiederholungsfenster, legitime Wiederholungen nach einem Ausfall ablehnen, und bezahlte Bestellungen würden nie bestätigt. Aktiviere sie erst, nachdem du in der Sandbox bestätigt hast, dass erneute Zustellungen einen neuen Zeitstempel tragen, und halte sie dann großzügig (z. B. eine Stunde) bei synchronisierten Serveruhren.
Beispiel (Node.js + Express)
import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
const app = express();
// Raw body for this route only; register it before any app-wide express.json().
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
} catch {
return res.sendStatus(401);
}
// Unique constraint on request_id; returns false if we've seen it.
const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
if (isNew) await jobs.enqueue("nomba-webhook", event); // process in the background
res.sendStatus(200);
});
Python (Flask/FastAPI/Django), PHP (Laravel/plain) und andere Node-Frameworks sind in references/frameworks.md.
Testen
- Teste den Handler mit dem vom SDK dokumentierten Testvektor: Body aus der
webhook-fixture.jsondes SDKs, SchlüsselSampleSignatureKey123, Headernomba-timestamp: 2025-09-29T10:51:44Z,nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=. Stelle sicher, dass ein falscher Schlüssel, ein manipulierter Body und fehlende Header abgelehnt werden und dass eine doppelterequestIdnur einmal verarbeitet wird. - End-to-End: Setze eine Test-Webhook-URL und einen Signaturschlüssel im Dashboard, mache deinen lokalen Server mit einem Tunnel zugänglich und tätige Sandbox-Zahlungen. Das Dashboard kann Zustellungen erneut pushen oder debuggen (https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks).
Checkliste
- Der rohe Body wird mit dem SDK verifiziert, bevor irgendein Parsing oder eine Geschäftslogik ausgeführt wird.
- Der Signaturschlüssel stammt aus der Umgebung, nicht aus dem Code.
- Schlechte Signaturen erhalten 401/400; es passiert nichts anderes.
-
requestIdwird mit einer eindeutigen Einschränkung dedupliziert; Duplikate erhalten dennoch 200. - Schwere Arbeiten laufen im Hintergrund; der Handler gibt schnell 200 zurück.
- Betrag und Währung werden vor der Erfüllung gegen die Bestellung geprüft.
- Rückerstattungen erfolgen nur bei
payout_refund(oder einer erneuten Abfrage, dieREFUNDzurückgibt). - Die Route ist vom CSRF-Schutz ausgeschlossen und verwendet HTTPS.