Skip to main content
Webhooks let WAVIS push events to your service when interesting things happen (compute jobs finish, usage thresholds hit, invoices issue). Each delivery is signed with HMAC-SHA256 so you can verify authenticity.

Event types

EventTriggered when
compute_completeA /compute/* job finishes
usage_alertAccount hits 50%, 80%, or 100% of monthly quota
limit_reachedDaily operation limit reached
budget_alertMonthly spend hits the user-set threshold
key_createdAn FHE key is generated or uploaded
key_deletedAn FHE key is deleted
key_rotatedAn API key is rotated (24 h grace)
invoice_createdStripe issues an invoice
invoice_paidStripe confirms payment
invoice_failedStripe payment fails
Subscribe to a subset, or pass an empty events array to receive all.

POST /api/v1/webhooks

Register a webhook endpoint. Auth: WRITE+

Request

POST /api/v1/webhooks
{
  "url": "https://example.com/wavis-webhook",
  "events": ["compute_complete", "usage_alert"],
  "secret": "your-min-32-char-shared-secret-here"
}
FieldTypeRequiredNotes
urlstringYesHTTPS only, ≤2048 chars, public IP only
eventsarray<string>NoEmpty = all events
secretstringNo≥32 chars; auto-generated if omitted
SSRF protections: The server rejects URLs that resolve to:
  • localhost, 127.0.0.0/8, ::1
  • .local, .internal, .intranet TLDs
  • Private IPv4 ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Link-local, loopback, reserved ranges
DNS resolution is re-checked at delivery time to prevent DNS rebinding.

Response — 201 Created

{
  "webhook_id": "wh_a1b2c3",
  "url": "https://example.com/wavis-webhook",
  "events": ["compute_complete", "usage_alert"],
  "created_at": "2026-04-28T12:00:00Z",
  "signature_info": {
    "algorithm": "HMAC-SHA256",
    "header": "X-WAVIS-Signature",
    "format": "sha256=<hex>"
  }
}
If secret was omitted in the request, a generated secret is returned in this response once only.

GET /api/v1/webhooks

List registered webhooks. Auth: Public (any valid key)

Request

GET /api/v1/webhooks?limit=20

Response — 200 OK

{
  "items": [
    {
      "webhook_id": "wh_a1",
      "url": "https://example.com/...",
      "events": ["compute_complete"],
      "created_at": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "total_count": 1
}

GET /api/v1/webhooks/{webhook_id}

Get details for one webhook. Auth: Public

Response — 200 OK

{
  "webhook_id": "wh_a1b2c3",
  "url": "https://example.com/...",
  "events": ["compute_complete"],
  "created_at": "..."
}

PATCH /api/v1/webhooks/{webhook_id}

Update URL or event subscriptions. Auth: WRITE+

Request

PATCH /api/v1/webhooks/wh_a1b2c3
{
  "url": "https://example.com/new-endpoint",
  "events": ["compute_complete", "limit_reached"]
}
Either field is optional — pass only what you want to change.

Response — 200 OK

Returns the full updated webhook object.

PATCH /api/v1/webhooks/{webhook_id}/regenerate-secret

Rotate the HMAC secret. The new secret is shown once only. Auth: WRITE+

Response — 200 OK

{
  "webhook_id": "wh_a1b2c3",
  "secret": "new-shared-secret-shown-once-only"
}
Update your verifier with the new secret promptly. The old secret stops working immediately — there is no grace period for webhook secrets.

DELETE /api/v1/webhooks/{webhook_id}

Unregister a webhook. Auth: WRITE+

Response — 200 OK

{
  "webhook_id": "wh_a1b2c3",
  "status": "deleted"
}

POST /api/v1/webhooks/test

Send a synthetic test event to all registered webhooks. Useful for verifying your verifier and HTTP plumbing. Auth: Public

Request

POST /api/v1/webhooks/test
{
  "event_type": "compute_complete"
}

Response — 200 OK

{
  "event_type": "compute_complete",
  "delivery_id": "del_test_a1",
  "results": {
    "total_webhooks": 2,
    "successes": 2,
    "failures": 0
  }
}

GET /api/v1/webhooks/events

Recent webhook delivery history (across all webhooks). Auth: Public

Request

GET /api/v1/webhooks/events?limit=50

Response — 200 OK

{
  "items": [
    {
      "delivery_id": "del_a1",
      "webhook_id": "wh_x1",
      "event_type": "compute_complete",
      "delivered_at": "...",
      "status_code": 200,
      "response_ms": 45,
      "attempts": 1
    }
  ]
}

GET /api/v1/webhooks/{webhook_id}/deliveries

Per-webhook delivery history. Auth: Public

Response — 200 OK

{
  "webhook_id": "wh_a1",
  "total": 12,
  "records": [
    {
      "delivery_id": "del_x1",
      "event_type": "compute_complete",
      "delivered_at": "...",
      "status_code": 200,
      "response_ms": 45,
      "attempts": 1,
      "request_body_sha256": "..."
    }
  ]
}

POST /api/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/replay

Replay a previously failed delivery. Idempotent — replaying a successful delivery is a no-op. Auth: WRITE+

Response — 200 OK

{
  "delivery_id": "del_x1",
  "replayed": true,
  "success": true,
  "error": null
}

Webhook payload format

WAVIS sends every delivery as a JSON POST to your URL:
POST /your-endpoint HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: WAVIS-Webhooks/1.0
X-WAVIS-Signature: sha256=a3f5b8...
X-WAVIS-Event-Type: compute_complete
X-WAVIS-Delivery-Id: del_a1b2

{
  "delivery_id": "del_a1b2",
  "event_type": "compute_complete",
  "account_id": "acct_xyz",
  "timestamp": "2026-04-28T12:00:00Z",
  "data": {
    "job_id": "op_a1b2c3",
    "result_ciphertext_id": "ct_result_xyz",
    "compute_time_ms": 8.2,
    "cost_jpy": 0.50
  }
}
The data field varies per event type — see schemas below.

Verifying signatures

The X-WAVIS-Signature header is sha256=<hex> where the hex is HMAC-SHA256 of the raw request body using your shared secret.

Python (Flask)

import hmac, hashlib
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = b"your-min-32-char-shared-secret-here"

@app.route("/wavis-webhook", methods=["POST"])
def webhook():
    sig_header = request.headers.get("X-WAVIS-Signature", "")
    if not sig_header.startswith("sha256="):
        abort(400)

    expected = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
    received = sig_header.removeprefix("sha256=")

    if not hmac.compare_digest(expected, received):
        abort(401)

    payload = request.get_json()
    print(f"event: {payload['event_type']}")
    return "", 200

Node.js (Express)

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SECRET = process.env.WAVIS_WEBHOOK_SECRET;

app.post("/wavis-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sigHeader = req.get("X-WAVIS-Signature") || "";
  const received = sigHeader.replace(/^sha256=/, "");

  const expected = createHmac("sha256", SECRET).update(req.body).digest("hex");

  if (!timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
    return res.status(401).end();
  }

  const payload = JSON.parse(req.body.toString());
  console.log(`event: ${payload.event_type}`);
  res.status(200).end();
});
Always use a constant-time comparison (hmac.compare_digest / timingSafeEqual) — == leaks info via timing.

Retry policy

Failed deliveries (HTTP 5xx, timeout, connection error) are retried with exponential backoff:
AttemptWait
1immediate
230 s
35 min
4give up
After 3 failed attempts, the delivery is marked failed and not retried automatically. Use POST /webhooks/{id}/deliveries/{delivery_id}/replay to retry manually. HTTP 4xx responses are NOT retried. Treat 401/404 as permanent failure on your side. Idempotency: the delivery_id is unique per attempt set; if you receive the same delivery_id twice, it’s a retry of the same logical event.

Per-event payloads

compute_complete

{
  "data": {
    "job_id": "op_a1b2c3",
    "operation": "matmul",
    "result_ciphertext_id": "ct_xyz",
    "compute_time_ms": 8.2,
    "cost_jpy": 0.50,
    "noise_budget_remaining": 78.5
  }
}

usage_alert

{
  "data": {
    "threshold_pct": 80,
    "monthly_fcu_used": 16000000,
    "monthly_fcu_limit": 20000000,
    "estimated_overage_usd": 0
  }
}

key_rotated

{
  "data": {
    "old_key_prefix": "wvs_live_old123",
    "new_key_prefix": "wvs_live_new456",
    "grace_period_ends_at": "2026-04-29T12:00:00Z"
  }
}

invoice_paid

{
  "data": {
    "invoice_id": "in_a1",
    "amount_usd_cents": 4900,
    "period_start": "2026-04-01",
    "period_end": "2026-04-30",
    "stripe_invoice_url": "https://invoice.stripe.com/..."
  }
}

Best practices

  1. Verify the signature on every request. Drop unsigned requests at the load balancer if possible.
  2. Respond within 5 s. Long-running work should be queued and acknowledged immediately with 200 OK.
  3. Handle replays. Use delivery_id as the idempotency key in your downstream system.
  4. Use a separate endpoint per environment. Keeps wvs_test_* events out of production webhooks.
  5. Rotate the secret quarterly. WAVIS doesn’t enforce it; your security policy should.
  6. Subscribe narrowly. Only the events you actually use — reduces load and noise.

Next Steps

Billing API

Subscription and invoice events

Compute API

The job_ids that webhook events reference