> ## Documentation Index
> Fetch the complete documentation index at: https://docs.restacked.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Use OpenStack webhooks to receive real-time notifications about important events.

Webhooks let you receive real-time notifications whenever something important
happens inside a project. This guide walks through creating subscriptions,
understanding deliveries, verifying signatures, and the catalog of events that
can be emitted by the system.

<Info>
  Events cover request lifecycle, auth/limits, routing outcomes, and (if you enable it) billing/ledger activity. Event
  keys are prefixless (for example,
  <code>proxy.request.started</code>, <code>balance.charge.created</code>).
</Info>

***

## 1. Authentication and Base URL

All webhook management endpoints live in the OpenStack API and require the
`Authorization: Bearer <OPENSTACK_SECRET>` header. Use the secret for the project
you want to subscribe on behalf of. Requests without a valid key return `401`.

Unless stated otherwise, example requests assume the API base URL is
`https://api.openstack.ai`.

***

## 2. Managing Subscriptions

### 2.1 Create a subscription

```http theme={null}
POST /v1/webhooks/subscriptions
Authorization: Bearer sk-openstack-v1-***
Content-Type: application/json

{
  "url": "https://example.com/openstack/events",
  "events": ["updated", "balance.*"],
  "description": "Send lifecycle + balance updates to billing service",
  "customHeaders": {
    "X-Source": "openstack"
  }
}
```

* `url` – HTTPS endpoint that will receive deliveries.
* `events` – array of event names. Use the wildcard `["*"]` to receive
  everything. The wildcard cannot be mixed with other names.
* `description` – optional, trimmed to 512 chars.
* `customHeaders` – optional object of additional headers, max 10 entries.
* `secret` – optional pre-shared secret. If omitted, one is generated.

**Response (201)**\
Returns the subscription without the full secret, except on creation where the
secret is included once:

```json theme={null}
{
  "subscription": {
    "id": "sub_123",
    "paywallId": "pw_456",
    "url": "https://example.com/openstack/events",
    "events": ["updated", "balance.*"],
    "isActive": true,
    "description": "Send lifecycle + balance updates to billing service",
    "customHeaders": {
      "X-Source": "openstack"
    },
    "createdAt": "2024-06-10T14:32:12.448Z",
    "updatedAt": "2024-06-10T14:32:12.448Z",
    "secret": "whsec_************************",
    "secretSuffix": "00f9a1",
    "createdBy": "user_abc",
    "consecutiveFailures": 0,
    "lastDeliveredAt": null
  }
}
```

> ⚠️ Store the `secret` securely. It is only returned on creation; subsequent
> reads show `secretSuffix` for diagnostics.

### 2.2 List subscriptions

```http theme={null}
GET /v1/webhooks/subscriptions?status=active
Authorization: Bearer sk-openstack-v1-***
```

* Optional `status` filter (`active` or `inactive`). The default returns both.

### 2.3 Fetch a single subscription

```http theme={null}
GET /v1/webhooks/subscriptions/{subscriptionId}
Authorization: Bearer sk-openstack-v1-***
```

### 2.4 Deactivate a subscription

```http theme={null}
DELETE /v1/webhooks/subscriptions/{subscriptionId}
Authorization: Bearer sk-openstack-v1-***
```

Subscriptions are soft-deactivated (`isActive: false`) so you can re-enable
them later via `PATCH /v1/webhooks/subscriptions/{id}` (not yet exposed via the
REST surface).

***

## 3. Inspecting Delivery Logs

```http theme={null}
GET /v1/webhooks/logs?subscriptionId=sub_123&status=error&limit=50
Authorization: Bearer sk-openstack-v1-***
```

Query parameters:

| Parameter        | Description                                                |
| ---------------- | ---------------------------------------------------------- |
| `subscriptionId` | Optional – restrict to a single subscription.              |
| `status`         | Optional – `pending`, `success`, or `error`.               |
| `eventType`      | Optional – filter by an event name defined in the catalog. |
| `limit`          | Optional – defaults to 20, max 100.                        |

Each log entry includes delivery metadata (`id`, `status`, retries, timing) and
the original event payload to help with debugging.

***

## 4. Delivery Payloads

When a webhook fires, we POST the following JSON document to your `url`:

```json theme={null}
{
  "delivery": {
    "id": "dly_abc123",
    "subscriptionId": "sub_123",
    "attempts": 1,
    "createdAt": "2024-06-10T14:32:25.133Z",
    "updatedAt": "2024-06-10T14:32:25.133Z"
  },
  "event": {
    "id": "evt_456",
    "paywallId": "pw_456",
    "ownerUserId": "user_abc",
    "type": "balance.deposit.created",
    "version": "1.0",
    "status": "pending",
    "createdAt": "2024-06-10T14:32:24.921Z",
    "trigger": {
      "source": "api",
      "actorType": "owner",
      "actorId": "user_abc"
    },
    "subject": {
      "type": "balance",
      "id": "activity_123",
      "externalId": "user_789"
    },
    "data": {
      "paywallId": "pw_456",
      "ownerUserId": "user_abc",
      "walletUserId": "user_789",
      "externalUserId": "user_789",
      "amount": "25",
      "currency": "usd",
      "source": "stripe",
      "activityId": "activity_123"
    },
    "metadata": {
      "paymentIntent": "pi_12345"
    }
  }
}
```

### 4.1 HTTP headers

Each delivery includes signature headers so you can validate authenticity:

| Header                 | Description                                                |
| ---------------------- | ---------------------------------------------------------- |
| `X-Paywalls-Event`     | `<eventType>@<version>` (e.g. `updated@1.0`).              |
| `X-Paywalls-Signature` | Hex-encoded HMAC-SHA256 signature of the raw request body. |
| `X-Paywalls-Timestamp` | ISO-8601 timestamp when the payload was generated.         |

**Verification steps**

1. Retrieve the subscription secret used for the delivery.
2. Compute `HMAC_SHA256(secret, raw_request_body)` and hex-encode it.
3. Compare to `X-Paywalls-Signature` using a constant-time comparison.
4. Optionally ensure `X-Paywalls-Timestamp` is within an acceptable window (e.g.
   5 minutes) to guard against replay attacks.

### 4.2 Example: Validate signatures in JavaScript

The snippet below shows a simple Node.js route that validates OpenStack signatures using the shared secret. It assumes you are using Express and have access to the raw request body.

```ts theme={null}
import crypto from 'node:crypto'
import type { Request, Response } from 'express'

function verifySignature(secret: string, payload: string, signature: string): boolean {
  const computed = crypto.createHmac('sha256', secret).update(payload).digest('hex')
  // Use timingSafeEqual to avoid leaking timing information
  const expected = Buffer.from(computed, 'hex')
  const received = Buffer.from(signature, 'hex')
  if (expected.length !== received.length) {
    return false
  }
  return crypto.timingSafeEqual(expected, received)
}

export async function handleOpenStackWebhook(req: Request, res: Response) {
  const secret = process.env.PAYWALLS_WEBHOOK_SECRET
  if (!secret) {
    res.status(500).send('Webhook secret not configured')
    return
  }

  const body = req.rawBody?.toString('utf8') ?? '' // Ensure raw body middleware is configured
  const signature = req.header('X-Paywalls-Signature') ?? ''
  const timestamp = req.header('X-Paywalls-Timestamp') ?? ''

  // Optional: reject stale timestamps (example uses 5 minutes)
  const FIVE_MINUTES_MS = 5 * 60 * 1000
  const timestampMs = Date.parse(timestamp)
  if (!timestamp || isNaN(timestampMs) || Math.abs(Date.now() - timestampMs) > FIVE_MINUTES_MS) {
    res.status(400).send('Stale or missing timestamp')
    return
  }

  if (!signature || !verifySignature(secret, body, signature)) {
    res.status(400).send('Invalid signature')
  }

  let payload
  try {
    payload = JSON.parse(body)
  } catch (err) {
    res.status(400).send('Invalid JSON payload')
    return
  }
  // Process the delivery payload...
  res.status(200).send('ok')
}
}
```

> Make sure your framework exposes the raw request body (Express requires enabling the `verify` option on the JSON body parser or a raw-body middleware).

***

## 5. Delivery Semantics & Retries

* Deliveries default to 30-second base backoff with jitter and double on each
  retry (configurable via environment variables).
* We retry up to `WEBHOOK_MAX_ATTEMPTS` (defaults to 8). Non-retryable status
  codes (`400`, `401`, `403`, `404`, `410`, `422`) end the delivery immediately
  with an error.

***

## 6. Next Steps

* Rotate webhook secrets periodically and update your consumer accordingly.
* Monitor `/v1/webhooks/logs` for failed deliveries; retry or investigate issues surfaced via the webhook delivery logs API.
* Use the event catalog to build targeted reactions rather than subscribing to `*` when possible—smaller workloads mean faster, more reliable processing.
