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

# Webhooks API

REST API endpoints for managing outbound webhook subscriptions.

## Overview

The Webhooks API lets you create and manage outbound webhook subscriptions. When events occur in your HolyDocs project (deployments, page changes, feedback, etc.), HolyDocs sends HTTP POST requests to your configured URLs with event payloads. Use webhooks to trigger CI/CD pipelines, sync data, send notifications, or build custom integrations.

**Base path:** `https://api.holydocs.com/api/v1/projects/:projectId/webhooks`

All webhook management endpoints require authentication with the `projects:write` scope.

## List Webhooks

Retrieve all webhook subscriptions for a project.

```bash
GET /api/v1/projects/:projectId/webhooks
```

Copy as context

**cURL**

```bash
    curl "https://api.holydocs.com/api/v1/projects/$PROJECT_ID/webhooks" \
      -H "Authorization: Bearer $HOLYDOCS_API_KEY"
    ```
  **CLI**

```bash
    holydocs api get "/projects/$PROJECT_ID/webhooks"
    ```
  **SDK**

```ts
    import { HolyDocs } from '@holydocs/sdk';
    const client = new HolyDocs({ apiKey: process.env.HOLYDOCS_API_KEY });
    const { data: webhooks } = await client.webhooks.list(projectId);
    ```

### Response

```json
{
  "data": [
    {
      "id": "wh_abc123",
      "url": "https://your-server.com/hooks/holydocs",
      "events": ["deployment.completed", "deployment.failed"],
      "active": true,
      "secret": "whsec_****************************cdef",
      "createdAt": "2026-03-15T08:00:00Z",
      "updatedAt": "2026-03-15T08:00:00Z",
      "lastDelivery": {
        "status": 200,
        "event": "deployment.completed",
        "deliveredAt": "2026-04-11T09:45:00Z",
        "duration": 230
      }
    },
    {
      "id": "wh_def456",
      "url": "https://hooks.slack.com/services/T00/B00/xxxx",
      "events": ["feedback.received"],
      "active": true,
      "secret": "whsec_****************************ghij",
      "createdAt": "2026-03-20T12:00:00Z",
      "updatedAt": "2026-04-01T15:30:00Z",
      "lastDelivery": {
        "status": 200,
        "event": "feedback.received",
        "deliveredAt": "2026-04-10T14:23:00Z",
        "duration": 180
      }
    }
  ]
}
```

The `secret` field is masked in list responses. The full secret is only returned once when the webhook is created.

## Create Webhook

Create a new webhook subscription with a target URL and a list of events to subscribe to.

```bash
POST /api/v1/projects/:projectId/webhooks
```

### Request Body

```json
{
  "url": "https://your-server.com/hooks/holydocs",
  "events": ["deployment.completed", "deployment.failed", "page.updated"],
  "description": "Notify CI/CD pipeline on deployments"
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | Yes | HTTPS URL to receive webhook payloads (must use HTTPS) |
| `events` | string[] | Yes | Array of event types to subscribe to (minimum 1) |
| `description` | string | No | Human-readable description (max 200 characters) |
| `secret` | string | No | Custom signing secret. If omitted, one is auto-generated. |
| `active` | boolean | No | Whether the webhook is active (default: `true`) |

### Response

```json
{
  "data": {
    "id": "wh_new789",
    "url": "https://your-server.com/hooks/holydocs",
    "events": ["deployment.completed", "deployment.failed", "page.updated"],
    "description": "Notify CI/CD pipeline on deployments",
    "active": true,
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "createdAt": "2026-04-11T10:00:00Z"
  }
}
```

The full `secret` value is only returned in the create response. Store it securely -- you will need it to verify webhook signatures. If you lose the secret, delete the webhook and create a new one.

**cURL**

```bash
    curl -X POST "https://api.holydocs.com/api/v1/projects/proj_abc123/webhooks" \
      -H "Authorization: Bearer hd_a1b2c3d4e5f67890a1b2c3d4e5f67890" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-server.com/hooks/holydocs",
        "events": ["deployment.completed", "deployment.failed"],
        "description": "CI/CD pipeline notifications"
      }'
    ```
  **JavaScript**

```javascript
    const response = await fetch(
      'https://api.holydocs.com/api/v1/projects/proj_abc123/webhooks',
      {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer hd_a1b2c3d4e5f67890a1b2c3d4e5f67890',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          url: 'https://your-server.com/hooks/holydocs',
          events: ['deployment.completed', 'deployment.failed'],
          description: 'CI/CD pipeline notifications'
        })
      }
    );
    const { data } = await response.json();
    // Store data.secret securely -- it is only shown once
    console.log(`Webhook ${data.id} created. Secret: ${data.secret}`);
    ```

## Update Webhook

Update an existing webhook's URL, events, or active status.

```bash
PATCH /api/v1/projects/:projectId/webhooks/:id
```

### Path Parameters

| Parameter | Type | Description |
|---|---|---|
| `projectId` | string | Project ID |
| `id` | string | Webhook ID |

### Request Body

All fields are optional. Only include the fields you want to update:

```json
{
  "url": "https://new-server.com/hooks/holydocs",
  "events": ["deployment.completed", "deployment.failed", "feedback.received"],
  "active": true
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | No | Updated HTTPS URL |
| `events` | string[] | No | Updated event list (replaces the existing list) |
| `description` | string | No | Updated description |
| `active` | boolean | No | Enable or disable the webhook |

### Response

```json
{
  "data": {
    "id": "wh_abc123",
    "url": "https://new-server.com/hooks/holydocs",
    "events": ["deployment.completed", "deployment.failed", "feedback.received"],
    "active": true,
    "updatedAt": "2026-04-11T10:30:00Z"
  }
}
```

Updating the `events` array replaces the entire list. To add a new event without removing existing ones, include all desired events in the array.

## Delete Webhook

Permanently delete a webhook subscription.

```bash
DELETE /api/v1/projects/:projectId/webhooks/:id
```

### Response

```json
{
  "data": {
    "deleted": true,
    "id": "wh_abc123"
  }
}
```

## Send Test Event

Send a test event to verify your webhook endpoint is working correctly. The test payload uses realistic sample data matching the selected event type.

```bash
POST /api/v1/projects/:projectId/webhooks/:id/test
```

### Request Body

```json
{
  "event": "deployment.completed"
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | string | No | Event type to simulate (default: first event in the webhook's subscription list) |

### Response

```json
{
  "data": {
    "delivered": true,
    "statusCode": 200,
    "duration": 245,
    "responseBody": "{\"ok\":true}",
    "event": "deployment.completed"
  }
}
```

If the endpoint returns a non-2xx status:

```json
{
  "data": {
    "delivered": false,
    "statusCode": 500,
    "duration": 1200,
    "responseBody": "Internal Server Error",
    "event": "deployment.completed",
    "error": "Endpoint returned HTTP 500"
  }
}
```

## Event Types

Subscribe to any combination of the following events:

### Deployment Events

| Event | Description | Payload Fields |
|---|---|---|
| `deployment.started` | A build has started processing | `deploymentId`, `projectId`, `branch`, `commitSha` |
| `deployment.completed` | A build completed successfully | `deploymentId`, `projectId`, `branch`, `commitSha`, `pagesBuilt`, `duration` |
| `deployment.failed` | A build failed | `deploymentId`, `projectId`, `branch`, `error` |

### Page Events

| Event | Description | Payload Fields |
|---|---|---|
| `page.created` | A new page was added to the docs | `pagePath`, `pageTitle`, `deploymentId` |
| `page.updated` | An existing page was modified | `pagePath`, `pageTitle`, `deploymentId`, `diff` |
| `page.deleted` | A page was removed from the docs | `pagePath`, `pageTitle`, `deploymentId` |

### Feedback Events

| Event | Description | Payload Fields |
|---|---|---|
| `feedback.received` | A reader submitted page feedback | `feedbackId`, `pagePath`, `rating`, `comment` |

### AI Events

| Event | Description | Payload Fields |
|---|---|---|
| `assistant.conversation.completed` | An AI chat conversation ended | `conversationId`, `messageCount`, `tokensUsed`, `satisfied` |
| `agent.job.completed` | An agent job finished processing | `jobId`, `suggestionsCount`, `status` |

## Webhook Payload Format

All webhook deliveries follow a consistent envelope format:

```json
{
  "id": "evt_abc123",
  "type": "deployment.completed",
  "projectId": "proj_xyz",
  "timestamp": "2026-04-11T09:45:00Z",
  "data": {
    "deploymentId": "dep_abc123",
    "branch": "main",
    "commitSha": "a1b2c3d4e5f6",
    "pagesBuilt": 42,
    "duration": 15000
  }
}
```

### Headers

Every webhook delivery includes these headers:

| Header | Description |
|---|---|
| `Content-Type` | `application/json` |
| `X-HolyDocs-Event` | Event type (e.g., `deployment.completed`) |
| `X-HolyDocs-Delivery` | Unique delivery ID for deduplication |
| `X-HolyDocs-Signature` | HMAC-SHA256 signature for verification |
| `X-HolyDocs-Timestamp` | Unix timestamp of the delivery |

## Verifying Signatures

Every webhook delivery is signed with your webhook secret using HMAC-SHA256. Always verify signatures to ensure payloads are authentic.

The signature is computed over the concatenation of the timestamp and the raw request body:

```
signature = HMAC-SHA256(secret, timestamp + "." + body)
```

**Node.js**

```javascript
    import crypto from 'crypto';

    function verifyWebhook(req, secret) {
      const signature = req.headers['x-holydocs-signature'];
      const timestamp = req.headers['x-holydocs-timestamp'];
      const body = req.body; // raw string body

      // Reject requests older than 5 minutes to prevent replay attacks
      const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
      if (age > 300) {
        throw new Error('Webhook timestamp too old');
      }

      const expected = crypto
        .createHmac('sha256', secret)
        .update(`${timestamp}.${body}`)
        .digest('hex');

      if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
        throw new Error('Invalid webhook signature');
      }

      return JSON.parse(body);
    }
    ```
  **Python**

```python
    import hmac
    import hashlib
    import time

    def verify_webhook(headers, body, secret):
        signature = headers.get("X-HolyDocs-Signature")
        timestamp = headers.get("X-HolyDocs-Timestamp")

        # Reject requests older than 5 minutes
        age = int(time.time()) - int(timestamp)
        if age > 300:
            raise ValueError("Webhook timestamp too old")

        expected = hmac.new(
            secret.encode(),
            f"{timestamp}.{body}".encode(),
            hashlib.sha256
        ).hexdigest()

        if not hmac.compare_digest(signature, expected):
            raise ValueError("Invalid webhook signature")

        return json.loads(body)
    ```

Always use constant-time comparison (`timingSafeEqual` in Node.js, `hmac.compare_digest` in Python) when verifying signatures. Standard string comparison is vulnerable to timing attacks.

## Retry Policy

Failed deliveries (non-2xx responses or timeouts) are retried with exponential backoff:

| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 1 hour |
| 5th retry | 4 hours |

After 5 failed retries, the delivery is marked as permanently failed. If a webhook accumulates 50 consecutive failures, it is automatically deactivated. You can reactivate it via the PATCH endpoint after fixing the receiving server.

Each delivery attempt includes the same `X-HolyDocs-Delivery` header. Use this value for deduplication on your server, since a single event may be delivered more than once due to retries.

## Webhook Limits by Plan

| Plan | Webhooks per Project | Events per Month |
|---|---|---|
| Free | 2 | 1,000 |
| Starter | 5 | 10,000 |
| Pro | 20 | 100,000 |
| Business | 50 | 1,000,000 |
| Enterprise | Unlimited | Unlimited |

## Error Codes

| Code | Status | Description |
|---|---|---|
| `NOT_FOUND` | 404 | Project or webhook not found |
| `VALIDATION_ERROR` | 400 | Invalid URL (must be HTTPS), empty events array, or unknown event type |
| `AUTH_ERROR` | 401 | Missing or invalid authentication |
| `FORBIDDEN` | 403 | API key lacks `projects:write` scope |
| `LIMIT_EXCEEDED` | 429 | Webhook limit or monthly event limit exceeded |
| `ALREADY_EXISTS` | 409 | A webhook with this URL already exists for this project |