Reseller API webhooks
Webhook events, payloads, HMAC signature verification in Python and Node, retries with backoff, URL safety rules and deduplication by event id.
Webhook events, payloads, HMAC signature verification in Python and Node, retries with backoff, URL safety rules and deduplication by event id.
All models, capabilities, and prices Documentation index for AI agents:https://oneprovider.dev/docs/llms.txt Create a webhook
POST /webhooks (scope webhooks): an https-only URL and a list of events. At most 5 webhooks.
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",
"secret": "whsec_..."
} The signing secret is shown once The secret field comes only in the create response. Store it on your server; a lost secret cannot be recovered, create the webhook again. Events
| Event | When | data |
|---|---|---|
key.created | A key was issued (from the API, cabinet, an order or a code). | key_id, name, limit_usd, customer_id, source (api, cabinet, order, code) |
key.paused | A key was paused. | key_id |
code.redeemed | A code was redeemed. | batch_id, code_tail, key_id, amount_usd |
order.completed | An order was completed. | order_id, external_order_id, kind, key_id, amount_usd |
balance.topped_up | The reseller balance was topped up. | amount_usd, order_id |
balance.low | The balance dropped below the threshold. | balance_usd, threshold_usd |
key.exhausted | A key limit is used up. Delivery is coming soon. | - |
Event data never contains secrets: no keys, tokens or codes.
Request and signature
We send a POST with a JSON body:
json{
"id": "evt_...",
"type": "order.completed",
"created_at": "2026-10-08T12:00:00Z",
"data": {
"order_id": "<order id>",
"external_order_id": "shop-1042",
"kind": "new_key",
"key_id": "<key id>",
"amount_usd": "10.00"
}
} | Header | Value |
|---|---|
X-OP-Signature | t=<unix>,v1=<hex> |
X-OP-Event | The event type. |
X-OP-Delivery | Equals the event id. |
User-Agent | OneProvider-Webhooks/1 |
v1 is the hex of an HMAC-SHA256 whose key is the webhook secret (whsec_...) and whose message is the string "<t>" + "." + raw_body.
Verifying the signature
- Read the raw request body: the signature covers the exact bytes, re-serialized JSON will not match.
- Recompute the HMAC and compare it with v1 in constant time (hmac.compare_digest, timingSafeEqual).
- Reject the request if |now - t| is more than 5 minutes, so an old signature cannot be replayed.
- Answer 2xx quickly (the timeout is 10 seconds) and do heavy work asynchronously.
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) Retries
Only a 2xx answer counts as delivered. Everything else (including 3xx, since redirects are not followed, plus timeouts and connection errors) is retried with exponential backoff: 1 minute, 2, 4 and so on, capped at 4 hours, until 24 hours after the event; then the delivery becomes failed. Every retry carries the same event id, so deduplicate on it: keep processed ids in a table with a unique index.
URL rules
- https only and a hostname; IP literals are rejected.
- Every address the name resolves to must not be private, loopback, link-local or reserved.
- The check runs on creation and on every delivery. A violation on creation gives 400
invalid_url.
Test and log
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" Delivery object: id, event_id, event, status (pending, delivered, failed), attempts, next_attempt_at, last_error, created_at. Delete a webhook: DELETE /webhooks/{id}.