Reseller-API: Webhooks
Webhook-Ereignisse, Payloads, HMAC-Signaturprüfung in Python und Node, Wiederholungen mit Backoff, URL-Sicherheitsregeln und Deduplizierung nach Event-ID.
Webhook-Ereignisse, Payloads, HMAC-Signaturprüfung in Python und Node, Wiederholungen mit Backoff, URL-Sicherheitsregeln und Deduplizierung nach Ereignis-ID.
Alle Modelle, Funktionen und Preise Dokumentationsindex für KI-Agenten:https://oneprovider.dev/docs/llms.txt Einen Webhook erstellen
POST /webhooks (Scope webhooks): eine URL nur über https und eine nicht leere Liste von Ereignissen. Höchstens 5 Webhooks. Ein optionaler Idempotency-Key: Eine Wiederholung liefert denselben Webhook, ohne das Secret. last_delivered_at ist der Zeitpunkt der letzten 2xx-Antwort Ihres Servers (null, bis es eine gab).
curl -s -X POST https://app.oneprovider.dev/partner/v1/webhooks \
-H "Authorization: Bearer $OP_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://shop.example.com/op-webhook","events":["order.completed","balance.low"]}' json {
"id": "<webhook id>",
"url": "https://shop.example.com/op-webhook",
"events": ["order.completed", "balance.low"],
"active": true,
"secret_last4": "a1b2",
"created_at": "2026-10-08T12:00:00Z",
"last_delivered_at": null,
"secret": "whsec_..."
} Das Signatur-Secret wird nur einmal angezeigt Das Feld secret kommt nur in der Antwort auf das Erstellen. Speichern Sie es auf Ihrem Server; ein verlorenes Secret lässt sich nicht wiederherstellen, erstellen Sie den Webhook neu. Ereignisse
| Ereignis | Wann | data |
|---|---|---|
key.created | Ein Schlüssel wurde ausgestellt (über die API, das Konto, eine Bestellung oder einen Code). | key_id, name, limit_usd, customer_id, source (api, cabinet, order, code) |
key.paused | Ein Schlüssel wurde pausiert (von Hand oder beim Sperren seines Kunden). | key_id, reason (nur bei einer Sperre: customer_blocked) |
key.resumed | Ein Schlüssel wurde fortgesetzt (von Hand oder beim Entsperren seines Kunden). | key_id, reason (nur bei einer Entsperrung: customer_unblocked) |
code.redeemed | Ein Code wurde eingelöst. | batch_id, code_tail, key_id, amount_usd |
order.completed | Eine Bestellung wurde abgeschlossen. | order_id, external_order_id, kind, key_id, amount_usd |
balance.topped_up | Das Reseller-Guthaben wurde aufgeladen. | amount_usd, order_id |
balance.low | Das Guthaben ist unter den Schwellenwert gefallen (von OneProvider festgelegt, standardmäßig 10 $, für alle gleich). Das Ereignis kommt einmal und erst wieder, nachdem das Guthaben über dem Schwellenwert lag. | balance_usd, threshold_usd |
key.exhausted | Ein Schlüssellimit ist aufgebraucht. Einmal pro Schlüssel und verkauftem Limit, innerhalb weniger Minuten; nach einer Aufladung kann es erneut ausgelöst werden. | key_id, limit_usd |
Die Ereignisdaten enthalten nie Secrets: keine Schlüssel, Tokens oder Codes. Der Test (POST /webhooks/{id}/test) sendet das Dienstereignis webhook.test mit den Daten {"message": "test"}; man kann es nicht abonnieren.
Anfrage und Signatur
Wir senden einen POST mit einem JSON-Body:
json{
"id": "evt_...",
"type": "order.completed",
"created_at": "2026-10-08T12:00:00Z",
"data": {
"order_id": 42,
"external_order_id": "shop-1042",
"kind": "new_key",
"key_id": "<key id>",
"amount_usd": "10.00"
}
} Der Body wird als kompaktes JSON mit alphabetisch sortierten Schlüsseln gesendet; prüfen Sie die Signatur über die Bytes genau so, wie sie ankamen, nicht über neu aufgebautes JSON.
| Header | Wert |
|---|---|
X-OP-Signature | t=<unix>,v1=<hex> |
X-OP-Event | Der Ereignistyp. |
X-OP-Delivery | Entspricht der Ereignis-ID. |
User-Agent | OneProvider-Webhooks/1 |
v1 ist der Hex-Wert eines HMAC-SHA256, dessen Schlüssel das Webhook-Secret (whsec_...) und dessen Nachricht der String "<t>" + "." + raw_body ist.
Die Signatur prüfen
- Lesen Sie den rohen Anfragekörper: Die Signatur deckt genau diese Bytes ab, neu serialisiertes JSON stimmt nicht überein.
- Berechnen Sie den HMAC neu und vergleichen Sie ihn in konstanter Zeit mit v1 (hmac.compare_digest, timingSafeEqual).
- Weisen Sie die Anfrage ab, wenn |now - t| größer als 5 Minuten ist, damit sich eine alte Signatur nicht erneut einspielen lässt.
- Antworten Sie schnell mit 2xx (das Timeout beträgt 10 Sekunden) und erledigen Sie aufwendige Arbeit asynchron.
Python
pythonimport hashlib
import hmac
import time
TOLERANCE = 300 # seconds
def verify(secret: str, header: str, raw_body: bytes) -> bool:
"""header is X-OP-Signature: t=<unix>,v1=<hex>; raw_body is the unparsed request body."""
parts = dict(p.strip().partition("=")[::2] for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > TOLERANCE:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) python import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
SECRET = os.environ["OP_WEBHOOK_SECRET"] # whsec_...
seen: set[str] = set() # use a database table with a unique index in production
@app.post("/op-webhook")
async def op_webhook(request: Request):
raw = await request.body() # the raw bytes, before any JSON parsing
if not verify(SECRET, request.headers.get("X-OP-Signature", ""), raw):
raise HTTPException(status_code=400, detail="bad signature")
event = json.loads(raw)
if event["id"] in seen: # retries resend the same event id
return {"ok": True}
seen.add(event["id"])
if event["type"] == "order.completed":
... # your logic
return {"ok": True} # any 2xx marks the event as delivered Node.js
javascriptimport crypto from "node:crypto"
import express from "express"
const SECRET = process.env.OP_WEBHOOK_SECRET // whsec_...
const TOLERANCE = 300 // seconds
const seen = new Set() // use a database table with a unique index in production
function verify(secret, header, raw) {
const parts = Object.fromEntries(
String(header).split(",").map((p) => p.trim().split("=")),
)
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE) return false
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + ".")
.update(raw)
.digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1 ?? "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
const app = express()
// express.raw keeps the body as a Buffer: do not use express.json() on this route
app.post("/op-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(SECRET, req.get("X-OP-Signature"), req.body)) return res.sendStatus(400)
const event = JSON.parse(req.body.toString("utf8"))
if (!seen.has(event.id)) {
seen.add(event.id) // retries resend the same event id
if (event.type === "order.completed") {
// your logic
}
}
res.sendStatus(200) // any 2xx marks the event as delivered
})
app.listen(3000) Wiederholungen
Nur eine 2xx-Antwort gilt als zugestellt. Alles andere (auch 3xx, da Weiterleitungen nicht verfolgt werden, sowie Timeouts und Verbindungsfehler) wird mit exponentiellem Backoff wiederholt: 1 Minute, 2, 4 und so weiter, der Abstand ist auf 4 Stunden begrenzt, bis 24 Stunden nach dem Ereignis; danach erhält die Zustellung den Status failed. Jede Wiederholung trägt dieselbe Ereignis-ID, deduplizieren Sie also danach: Speichern Sie verarbeitete IDs in einer Tabelle mit eindeutigem Index.
URL-Regeln
- Nur https, Port 443 (jeder andere Port ergibt 400 invalid_url) und ein Hostname; IP-Adressen werden abgelehnt. Die URL hat höchstens 500 ASCII-Zeichen, ohne Benutzernamen, Passwort oder Fragment.
- Keine Adresse, in die der Name aufgelöst wird, darf privat, Loopback, Link-Local oder reserviert sein.
- Die Prüfung läuft beim Erstellen und bei jeder Zustellung. Ein Verstoß beim Erstellen ergibt 400
invalid_url.
Test und Protokoll
bashcurl -s -X POST https://app.oneprovider.dev/partner/v1/webhooks/<webhook id>/test -H "Authorization: Bearer $OP_PARTNER_TOKEN"
curl -s "https://app.oneprovider.dev/partner/v1/webhooks/<webhook id>/deliveries?limit=20" -H "Authorization: Bearer $OP_PARTNER_TOKEN" Zustellungsobjekt: id, event_id, event, status (pending, delivered, failed), attempts, next_attempt_at, last_error (http_<status>, timeout, blocked_address oder der Name des Verbindungsfehlers), last_status (der HTTP-Status, mit dem Ihr Server geantwortet hat, null, wenn er nicht geantwortet hat), last_body (der Anfang der Antwort, bis zu 300 Zeichen, in einer Zeile), created_at. Einen Webhook löschen: DELETE /webhooks/{id}. Tests und manuelle Wiederholungen (im Konto) sind zusammen auf 6 pro Minute begrenzt; wiederholen lässt sich nur ein nicht zugestelltes Ereignis, ein zugestelltes ergibt 409 already_delivered. Die manuelle Wiederholung gibt es nur im Konto, nicht mit einem Token.