Webhooks, Idempotency & OpenAPI
Webhooks, Idempotency & OpenAPI
The canonical reference for cross-cutting behaviours. Other docs point
here. Auth: Authorization: Bearer wsp_β¦.
Webhook endpoints have no granular permission token β an API key needs the
allscope (a scoped key β403 "API key is not scoped for 'webhooks'"). See Authentication.
Webhook responses use the lean envelope
{ "success": β¦, "error": β¦, "code": β¦ }β nomessage/statusfields (differs from most panel endpoints; see Error Handling).
Webhooks β /api/v1/webhooks
List β GET /api/v1/webhooks/
Response 200 (verified live β paginated; non-admin sees only own):
{ "success": true, "data": [],
"meta": { "total": 0, "per_page": 50, "current_page": 1, "last_page": 1 } }
Create β POST /api/v1/webhooks/
The secret is server-generated (not supplied by you) and shown
only in the create response.
Request:
{ "url": "https://example.com/hook",
"events": ["user.created", "domain.*", "backup.completed"],
"enabled": true }
| Field | Required | Note |
|---|---|---|
url |
β | scheme must be http or https |
events |
β | β₯1 pattern; * and prefix.* wildcards allowed |
enabled |
β | default true |
Response 201 (verified live):
{ "success": true,
"data": {
"id": "76f9d4cca73ac9c9ee05649421570c01",
"owner": "admin",
"url": "https://example.com/hook",
"events": ["user.created", "domain.*", "backup.completed"],
"secret": "96bc7a66c6e9e57bee747a20c072f3f1c85f1fc740b237e7fc6e679a486b59e0",
"enabled": true,
"created_at": "2026-05-19T07:11:15+07:00",
"last_fired_at": "0001-01-01T00:00:00Z",
"failure_count": 0 } }
Validation 400 (verified live): URL is required /
At least one event pattern is required /
scheme must be http or https:
{ "success": false, "error": "URL is required", "code": "VALIDATION_ERROR" }
Get / update / delete
GET /api/v1/webhooks/:id β same shape, but secret is redacted
(verified live): "secret": "<redacted>".
PATCH /api/v1/webhooks/:id β send only changed keys
(url, events, enabled); returns the updated (redacted) hook.
DELETE /api/v1/webhooks/:id β 200 (verified live):
{ "success": true, "message": "Webhook deleted" }
Missing id β 404 { "success": false, "error": "Webhook not found", "code": "NOT_FOUND" }. POST/PATCH/DELETE also require step-up
re-auth for session callers (API-key callers are exempt).
Delivery signature
Each delivery is POSTed to your url with header
X-WisPanel-Signature: sha256=<hmac> where the HMAC is
HMAC-SHA256(secret, raw_body). Verify before trusting:
$calc = hash_hmac('sha256', $rawBody, $secret);
hash_equals($calc, substr($_SERVER['HTTP_X_WISPANEL_SIGNATURE'], 7));
Idempotency-Key
Send Idempotency-Key: <uuid> on POST/PUT/PATCH/DELETE. Verified
live behaviour within the 24h window:
| Replay | Result (verified live) |
|---|---|
| Same key + same body | the original response is replayed (same id, HTTP 201) with header Idempotency-Replayed: true β no duplicate created |
| Same key + different body | 409 { "success": false, "code": "IDEMPOTENCY_KEY_CONFLICT", "error": "Idempotency-Key reused with a different request" } |
| After 24h | treated as a fresh request |
Only 2xx are cached; failures always re-run.
OpenAPI
GET /openapi.yaml and GET /api/v1/openapi.yaml (no auth) β
200, Content-Type: application/yaml, ~13.6 KB, beginning
openapi: 3.0.3 (title "WisPanel API"). Use it to generate clients.
Rate limits
/api/v1/auth/* responses carry (verified live, exact casing):
X-Ratelimit-Limit: 20
X-Ratelimit-Window: 60
X-Ratelimit-Remaining: 19
X-Ratelimit-Reset: 60
X-Ratelimit-Reset is seconds until the window resets (not a unix
timestamp). On 429 a Retry-After header is also sent.
Auth & key scope β Authentication. Error envelopes β Error Handling.