Webhooks
Die Wahrheitsquelle Ihrer Integration: jede Zustandsänderung wird Ihnen zugestellt, signiert, mit automatischer Neuzustellung.
Einen Endpunkt anlegen
POST /v1/webhooks
{
"url": "https://ihreseite.de/webhook/neopays",
"description": "produktion",
"subscribed_events": ["payment.success", "refund.success"]
}
Die Antwort trägt das Signaturgeheimnis. Leeres subscribed_events = alle Ereignisse; ein
Ereignis außerhalb des Katalogs wird abgelehnt statt gespeichert, weil es auf eine Zustellung warten
würde, die nichts auslöst.
Dieser Aufruf stellt sicher, er legt nicht immer an. Eine bereits registrierte URL kommt mit
200zurück, statt dupliziert zu werden — ein zweimal ausgeführtes Provisionierungsskript hinterlässt EINEN Endpunkt, nicht zwei, die jedes Ereignis doppelt empfangen. Das Geheimnis wird in beiden Fällen zurückgegeben, denn der legitime Grund für einen erneuten Aufruf ist, es verloren zu haben.
GET /v1/webhooks — Ihre Endpunkte und ihre Zähler
GET /v1/webhooks/{id} — ein Endpunkt
PATCH /v1/webhooks/{id} — teilweise Änderung
DELETE /v1/webhooks/{id} — endgültige Löschung
GET /v1/webhooks/{id}/secret — das Geheimnis erneut lesen
POST /v1/webhooks/test — Testzustellung an alle Ihre aktiven Endpunkte
Das PATCH ändert nur, was Sie senden: is_active wegzulassen deaktiviert den Endpunkt nicht. Um
Zustellungen zu stoppen, ohne den Endpunkt und sein Geheimnis zu verlieren, senden Sie
is_active: false, statt zu löschen.
Höchstens zehn Endpunkte je Konto. Das Dashboard kann all das ebenfalls, und nur es kann eine fehlgeschlagene Zustellung wiederholen und ein Geheimnis rotieren.
Die Ereignisse
| Ereignis | Wann |
|---|---|
payment.initiated |
Einzug angelegt |
payment.pending |
Wartet auf die Bestätigung des Zahlers |
payment.success |
Eingezogen — das maßgebliche Ereignis |
payment.failed |
Fehlschlag |
payout.success / payout.failed / payout.cancelled |
Lebenszyklus einer Auszahlung |
payout.refunded |
Auszahlung im Nachhinein vom Rail zurückgegeben |
refund.success / refund.failed |
Lebenszyklus einer Rückerstattung |
Der Umschlag ist immer derselbe:
{
"event": "payment.success",
"created": "2026-08-24T12:00:00Z",
"data": {
"id": "op_01J8Z9K2QW",
"reference": "NP-...",
"merchant_reference": "bestellung-1042",
"amount": "5000",
"currency": "XOF",
"status": "success"
}
}
Der Webhook-Body hat nicht dieselbe Form wie die API-Antwort. Er trägt
amountundcurrency, wo die APIamount_minorundasset_idträgt, undmerchant_reference, wo sieclient_refträgt. Das ist ein eigener Vertrag, früher geschrieben und seither unverändert: nehmen Sie nicht an, dass sich ein Objekt des einen wie ein Objekt des anderen dekodieren lässt.
Die Signatur prüfen
Jede Zustellung trägt den Header:
X-NeoPays-Signature: t=<Unix-Zeitstempel>,v1=<hmac>
wobei hmac = HMAC-SHA256(Geheimnis, "<t>.<Rohbody>"), hexadezimal. Prüfen Sie in drei Schritten:
const crypto = require('node:crypto')
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
Der ROHE Body, nicht der neu serialisierte. Berechnen Sie den HMAC über die empfangenen Bytes, vor jedem JSON-Parsing. Ein erneutes
JSON.stringifykann Schlüssel umordnen und die Signatur ungültig machen.
Während einer Geheimnis-Rotation trägt der Header ZWEI
v1=-Einträge, das aktuelle Geheimnis zuerst. Prüfen Sie alle, nicht nur den ersten: an dem Tag, an dem Sie Ihre Konfiguration auf das neue Geheimnis umstellen, bevor die Umstellung auf Plattformseite fertig ist, stammt die erste Signatur vom alten Geheimnis — und ein Prüfer, der beim ersten stehen bleibt, lehnt dann 100 % der Zustellungen ab.
Neuzustellung
Antworten Sie innerhalb weniger Sekunden mit 2xx (bestätigen, danach verarbeiten). Jede andere
Antwort löst die Wiederholungsleiter aus: insgesamt 5 Versuche, nach 60 s, 5 min, 30 min und 2 h.
Ein Endpunkt, der 20 Zustellungen in Folge scheitern lässt, wird eine Stunde ausgesetzt (die Zustellungen laufen von selbst wieder an); ein einziger Erfolg — auch eine Testzustellung aus dem Dashboard — stellt ihn sofort wieder her.
Idempotenz auf der Empfängerseite
Zustellungen sind at-least-once: dasselbe Ereignis kann zweimal ankommen. Entdoppeln Sie über das
Paar (event, data.id) oder protokollieren Sie die bereits verarbeiteten Ereigniskennungen.