neo-pays

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 200 zurü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 amount und currency, wo die API amount_minor und asset_id trägt, und merchant_reference, wo sie client_ref trä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.stringify kann 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.