OneProvider

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).

bash
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

Webhook-Ereignisse
EreignisWanndata
key.createdEin 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.pausedEin Schlüssel wurde pausiert (von Hand oder beim Sperren seines Kunden).key_id, reason (nur bei einer Sperre: customer_blocked)
key.resumedEin Schlüssel wurde fortgesetzt (von Hand oder beim Entsperren seines Kunden).key_id, reason (nur bei einer Entsperrung: customer_unblocked)
code.redeemedEin Code wurde eingelöst.batch_id, code_tail, key_id, amount_usd
order.completedEine Bestellung wurde abgeschlossen.order_id, external_order_id, kind, key_id, amount_usd
balance.topped_upDas Reseller-Guthaben wurde aufgeladen.amount_usd, order_id
balance.lowDas 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.exhaustedEin 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 der Zustellung
HeaderWert
X-OP-Signaturet=<unix>,v1=<hex>
X-OP-EventDer Ereignistyp.
X-OP-DeliveryEntspricht der Ereignis-ID.
User-AgentOneProvider-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

  1. Lesen Sie den rohen Anfragekörper: Die Signatur deckt genau diese Bytes ab, neu serialisiertes JSON stimmt nicht überein.
  2. Berechnen Sie den HMAC neu und vergleichen Sie ihn in konstanter Zeit mit v1 (hmac.compare_digest, timingSafeEqual).
  3. Weisen Sie die Anfrage ab, wenn |now - t| größer als 5 Minuten ist, damit sich eine alte Signatur nicht erneut einspielen lässt.
  4. Antworten Sie schnell mit 2xx (das Timeout beträgt 10 Sekunden) und erledigen Sie aufwendige Arbeit asynchron.

Python

python
import 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

javascript
import 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

bash
curl -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.

Zuletzt aktualisiert: 2026-11-08 Fehler gefunden? @Oneproviderhelpbot