Webhooks
Status: Complete — All content endpoints trigger webhooks.
Overview
The webhook service is fully integrated with all content CRUD and publish endpoints:
- Service:
apps/api/src/services/webhook.ts - Routes:
apps/api/src/routes/admin/webhooks.ts - Repositories:
@tentocms/db(WebhookRepository, WebhookDeliveryRepository)
Available Functions
triggerWebhooks()
Triggers webhooks for a content event. This function:
- Finds all active webhooks subscribed to the event
- Creates delivery records in the database
- Dispatches async HTTP requests via
ctx.waitUntil()
Signature:
triggerWebhooks(
db: D1Database,
projectId: string,
event: WebhookEvent,
data: unknown,
ctx: ExecutionContext
): Promise<void>
generateSignature()
Generates HMAC-SHA256 signature for payload verification.
processWebhookRetries()
Processes failed webhook deliveries with exponential backoff. Called by cron handler.
Endpoint → Event Mapping
Pages (apps/api/src/routes/pages.ts)
| Endpoint | Event | Data |
|---|---|---|
POST /admin/pages (create) | page.created | Created page object |
PUT /admin/pages/:id (update) | page.updated | Updated page object |
POST /admin/pages/:id/resolve-conflict | page.updated | Resolved page object |
POST /admin/pages/:id/clone | page.created | Cloned page object |
POST /admin/pages/:id/versions/:v/restore | page.updated | Reverted page object |
DELETE /admin/pages/:id | page.deleted | { id, name, slug } |
POST /admin/pages/:id/publish | page.published | Published page object |
POST /admin/pages/:id/unpublish | page.unpublished | Unpublished page object |
POST /admin/pages/bulk-delete | page.deleted per item | { id } per deleted page |
POST /admin/pages/bulk-publish | page.published per item | { id } per published page |
POST /admin/pages/bulk-unpublish | page.unpublished per item | { id } per unpublished page |
Not triggered for: autosave (too frequent, not a deliberate save), schedule/unschedule (no content change, scheduling metadata only).
Collection Items (apps/api/src/routes/collection-items.ts)
| Endpoint | Event | Data |
|---|---|---|
POST /admin/collection-items (create) | collection_item.created | Created item object |
PUT /admin/collection-items/:id (update) | collection_item.updated | Updated item object |
POST /admin/collection-items/:id/resolve-conflict | collection_item.updated | Resolved item object |
POST /admin/collection-items/:id/clone | collection_item.created | Cloned item object |
POST /admin/collection-items/:id/versions/:v/restore | collection_item.updated | Reverted item object |
DELETE /admin/collection-items/:id | collection_item.deleted | { id, name, slug } |
POST /admin/collection-items/:id/publish | collection_item.published | Published item object |
POST /admin/collection-items/:id/unpublish | collection_item.unpublished | Unpublished item object |
POST /admin/collection-items/bulk-delete | collection_item.deleted per item | { id } per deleted item |
POST /admin/collection-items/bulk-publish | collection_item.published per item | { id } per published item |
POST /admin/collection-items/bulk-unpublish | collection_item.unpublished per item | { id } per unpublished item |
Not triggered for: autosave (too frequent, not a deliberate save), schedule/unschedule (no content change, scheduling metadata only).
Media (apps/api/src/routes/media.ts)
| Endpoint | Event | Data |
|---|---|---|
POST /admin/media/upload | media.uploaded | Created media object |
POST /admin/media/bulk-upload | media.uploaded per file | Each media object |
DELETE /admin/media/:id | media.deleted | { id, filename, originalFilename } |
Not triggered for: metadata update (PUT /admin/media/:id), move (POST /admin/media/:id/move), folder rename (PUT /admin/media/folders/:name) — no matching event types defined for these operations.
Available Webhook Events
All events are defined in @tentocms/shared:
page.createdpage.updatedpage.publishedpage.unpublishedpage.deletedcollection_item.createdcollection_item.updatedcollection_item.publishedcollection_item.unpublishedcollection_item.deletedmedia.uploadedmedia.deleted
Webhook Payload Structure
All webhooks receive this payload structure:
{
event: 'page.published',
timestamp: '2024-01-15T10:30:00.000Z',
data: {
// The actual content data (page, collection item, media, etc.)
}
}
HTTP Headers
Webhook requests include these headers:
Content-Type: application/jsonX-Webhook-Signature: sha256={hmac_signature}X-Webhook-Event: {event_type}X-Webhook-Delivery-Id: {delivery_uuid}User-Agent: TentoCMS-Webhook/1.0- Any custom headers configured on the webhook
Retry Logic
Failed deliveries are automatically retried with exponential backoff:
- 1 minute
- 5 minutes
- 15 minutes
- 1 hour
- 4 hours
After 5 failed attempts, the delivery is marked as exhausted.
Testing
Use the test endpoint to verify webhook configuration:
POST /api/v1/admin/webhooks/:id/test
This sends a test page.updated event with sample data.
Security
- Secrets are only shown on webhook creation and regeneration
- Secrets are masked in list views (shows last 8 chars)
- All webhook operations require
developerrole - HMAC-SHA256 signatures allow receivers to verify authenticity
Performance & Resilience
- Webhook delivery is async via
ctx.waitUntil()— doesn't block API responses - All
triggerWebhooks()calls are wrapped in try/catch — webhook failures never break content operations - Bulk operations fire webhooks in parallel via
Promise.allSettled - 30 second timeout per delivery attempt
- Response body captured up to 10KB
- Cron handler processes retries every minute

