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.
bashGET /api/v1/projects/:projectId/webhooks
bashcurl "https://api.holydocs.com/api/v1/projects/$PROJECT_ID/webhooks" \ -H "Authorization: Bearer $HOLYDOCS_API_KEY"
bashholydocs api get "/projects/$PROJECT_ID/webhooks"
tsimport { 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.
bashPOST /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.
bashcurl -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" }'
javascriptconst 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 onceconsole.log(`Webhook ${data.id} created. Secret: ${data.secret}`);
Update Webhook
Update an existing webhook's URL, events, or active status.
bashPATCH /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.
bashDELETE /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.
bashPOST /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:
textsignature = HMAC-SHA256(secret, timestamp + "." + body)
javascriptimport 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);}
pythonimport hmacimport hashlibimport timedef 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 |