Webhooks API

Manage webhook endpoints and receive real-time event notifications.

All endpoints require:

  • Authorization: Bearer <token> (JWT with admin role)
  • X-Bulwark-Tenant: <tenant-id> header
  • X-Bulwark-App-Id: <app-id> header — Optional. Filters webhooks to a specific application.

Endpoints

List webhooks

GET /api/v1/webhooks

Returns all registered webhook endpoints for the tenant.

Response200 OK

{
  "webhooks": [
    {
      "id": "wh_01j...",
      "url": "https://myapp.com/webhooks/bulwark",
      "description": "Production webhook",
      "events": ["user.created", "user.login"],
      "enabled": true,
      "created_at": "2026-03-01T12:00:00Z"
    }
  ]
}

Create webhook

POST /api/v1/webhooks

Request body

| Field | Type | Required | Description | |-------|------|----------|-------------| | url | string | Yes | HTTPS endpoint to receive events | | events | string[] | Yes | Event types to subscribe to | | description | string | No | Human-readable label |

{
  "url": "https://myapp.com/webhooks/bulwark",
  "events": ["user.created", "user.login", "user.deleted"],
  "description": "Production webhook"
}

Response201 Created

{
  "id": "wh_01j...",
  "url": "https://myapp.com/webhooks/bulwark",
  "events": ["user.created", "user.login", "user.deleted"],
  "secret": "whsec_a3f9..."
}

The secret field is returned only on creation. Store it immediately — it is hashed and cannot be retrieved again. Use it to verify incoming webhook signatures.


Delete webhook

DELETE /api/v1/webhooks/{id}

Response200 OK

{ "status": "webhook deleted" }

Rotate secret

POST /api/v1/webhooks/{id}/rotate-secret

Generates a new signing secret and invalidates the old one immediately. The new secret is returned once — store it; any receiver still verifying with the old secret will reject subsequent deliveries.

Response200 OK

{ "secret": "<new-raw-secret>" }

Send test delivery

POST /api/v1/webhooks/{id}/test

Sends a synthetic webhook.test event to the endpoint (ignoring the webhook's enabled flag) and records it like a normal delivery. Use it to confirm reachability and signature verification.

Response202 Accepted

{ "status": "sent" }

The test payload:

{
  "event": "webhook.test",
  "timestamp": "2026-06-01T12:00:00Z",
  "data": {
    "test": true,
    "message": "Bulwark test delivery — your webhook is reachable.",
    "webhook": "whk_01j...",
    "triggered": "2026-06-01T12:00:00Z"
  }
}

List recent deliveries

GET /api/v1/webhooks/deliveries

Returns the 50 most recent delivery attempts across the tenant's webhooks.

Response200 OK

{
  "deliveries": [
    {
      "id": "whd_01j...",
      "webhook_id": "whk_01j...",
      "event": "user.created",
      "status_code": 200,
      "success": true,
      "duration_ms": 142,
      "attempted_at": "2026-06-01T12:00:00Z"
    }
  ]
}

status_code is 0 when no response was received (e.g. a network error or timeout).


Retry a delivery

POST /api/v1/webhooks/deliveries/{id}/retry

Re-sends a past delivery's original event + payload with a fresh timestamp and signature. The target webhook must still exist and be enabled. A new delivery record is written for the retry.

Response202 Accepted

{ "status": "retried" }

Returns 422 if the original delivery can't be found, or the webhook no longer exists or is disabled.


Events

| Event | Triggered when | |-------|---------------| | user.created | A new user account is registered | | user.login | A user successfully authenticates | | user.password_changed | A user changes their password | | user.email_verified | A user verifies their email address | | user.deleted | A user account is deleted |


Payload format

Every webhook delivery sends a POST request to your endpoint with a JSON body:

{
  "event": "user.created",
  "timestamp": "2026-03-20T10:05:00Z",
  "data": {
    "id": "usr_01j...",
    "email": "[email protected]",
    "display_name": "Jane Doe",
    "email_verified": false,
    "created_at": "2026-03-20T10:05:00Z"
  }
}

The structure of data varies by event type and matches the corresponding user or session object.


Signature verification

Each delivery includes an X-Bulwark-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using the webhook secret.

Algorithm: HMAC-SHA256(secret, body)

Header format: sha256=<hex-digest>

Verification examples

Node.js

import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(body: string, signature: string, secret: string): boolean {
  const expected = createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  const expectedBuffer = Buffer.from(`sha256=${expected}`, "utf8");
  const signatureBuffer = Buffer.from(signature, "utf8");

  if (expectedBuffer.length !== signatureBuffer.length) return false;
  return timingSafeEqual(expectedBuffer, signatureBuffer);
}

// In your route handler:
app.post("/webhooks/bulwark", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-bulwark-signature"] as string;
  const isValid = verifyWebhook(req.body.toString(), signature, process.env.BULWARK_WEBHOOK_SECRET!);

  if (!isValid) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body.toString());
  // handle event...
  res.status(200).json({ received: true });
});

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func verifyWebhook(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := fmt.Sprintf("sha256=%s", hex.EncodeToString(mac.Sum(nil)))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Always use a constant-time comparison (timingSafeEqual / hmac.Equal) to prevent timing attacks.


Retry behavior

Failed deliveries (non-2xx response or timeout) are retried up to 5 times with exponential backoff: 30s, 2m, 10m, 30m, 2h. After all retries are exhausted the delivery is marked as failed and no further attempts are made.

Respond with 2xx as quickly as possible. Process events asynchronously if your handler does significant work.