Webhooks
Register outgoing HTTP webhooks that fire when API write events happen (page/media/user mutations, config updates, GPM installs, Grav upgrades). Each webhook has a URL, an event filter list, optional custom headers, a generated secret for HMAC signing, and a delivery log. Permissions: api.webhook...
Register outgoing HTTP webhooks that fire when API write events happen (page/media/user mutations, config updates, GPM installs, Grav upgrades). Each webhook has a URL, an event filter list, optional custom headers, a generated secret for HMAC signing, and a delivery log.
Permissions: api.webhooks.read / api.webhooks.write. Secrets are generated by the server and always redacted in API responses, except once in the response that creates the webhook.
Valid event names
* (all), page.created, page.updated, page.deleted, page.moved, page.translated, pages.reordered, media.uploaded, media.deleted, user.created, user.updated, user.deleted, config.updated, gpm.installed, gpm.removed, grav.upgraded.
Deliveries
Each delivery is a JSON POST of {"event": "...", "timestamp": "<ISO 8601>", "data": {...}, "webhook_id": "wh_..."}. Grav adds these headers to every delivery:
| Header | Value |
|---|---|
X-Grav-Signature |
Hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret |
X-Grav-Event |
The webhook event name, e.g. page.updated |
X-Grav-Delivery |
A unique delivery ID (dlv_...) |
A webhook's custom headers can set other headers, including Content-Type and User-Agent, but can't replace these three: creating or updating a webhook with one of them returns 422. Only http and https URLs are accepted, and a URL that resolves to a private, loopback, link-local or reserved address is refused, both when saved and again at delivery time. Redirects are not followed.
A webhook that fails 5 deliveries in a row is disabled automatically, with a disabled_reason; a successful delivery resets the count. The last 50 deliveries of each webhook are kept.
List Webhooks
/webhooks
{"data": [{"id": "wh_abc123", "url": "https://example.com/hooks/grav", "secret": "whsec_************************************************a1b2", "events": ["page.created", "page.updated"], "enabled": true, "headers": {}, "created": 1774008000, "failure_count": 0}]}
Response Codes
Create Webhook
/webhooks
Parameters
| Name | Type | Description |
|---|---|---|
| url required | string | Absolute HTTP(S) URL to POST payloads to. |
| events optional | array | Event filter, as a JSON array of event names. Use `["*"]` to receive every event, or list specific events. Defaults to `["*"]`. Anything other than an array of valid names is a 422. |
| headers optional | object | Custom headers to send with every delivery, as an object of header names to values. `X-Grav-Signature`, `X-Grav-Event` and `X-Grav-Delivery` (in any letter case) are refused with a 422, as are names or values containing line breaks. |
| enabled optional | boolean | Defaults to true. |
{"url": "https://example.com/hooks/grav", "events": ["page.created", "page.updated"], "enabled": true}
{"data": {"id": "wh_abc123", "url": "https://example.com/hooks/grav", "secret": "whsec_0123456789abcdef0123456789abcdef0123456789abcdef", "events": ["page.created", "page.updated"], "enabled": true, "headers": {}, "created": 1774526400, "failure_count": 0}}
Response Codes
Get Webhook
/webhooks/{id}
Parameters
| Name | Type | Description |
|---|---|---|
| id required | string | Webhook id (path param). |
{"data": {"id": "wh_abc123", "url": "https://example.com/hooks/grav", "secret": "whsec_************************************************a1b2", "events": ["page.created", "page.updated"], "enabled": true, "headers": {}, "created": 1774008000, "failure_count": 0}}
Response Codes
Update Webhook
/webhooks/{id}
Parameters
| Name | Type | Description |
|---|---|---|
| id required | string | Webhook id (path param). |
| url optional | string | New URL. |
| events optional | array | Replacement event filter (a JSON array of event names). |
| headers optional | object | Replacement custom headers. The three `X-Grav-*` headers Grav sets are refused with a 422. |
| enabled optional | boolean | Enable/disable the webhook without deleting it. |
{"url": "https://example.com/hooks/grav-v2", "events": ["page.created", "page.updated", "page.deleted"]}
{"data": {"id": "wh_abc123", "url": "https://example.com/hooks/grav-v2", "secret": "whsec_************************************************a1b2", "events": ["page.created", "page.updated", "page.deleted"], "enabled": true, "headers": {}, "created": 1774008000, "failure_count": 0}}
Response Codes
Delete Webhook
/webhooks/{id}
Parameters
| Name | Type | Description |
|---|---|---|
| id required | string | Webhook id (path param). |
Response Codes
Delivery Log
/webhooks/{id}/deliveries
Parameters
| Name | Type | Description |
|---|---|---|
| id required | string | Webhook id (path param). |
| page optional | integer | Page number (default 1). |
| per_page optional | integer | Items per page (default `plugins.api.pagination.default_per_page`, 20). |
{"data": [{"id": "dlv_9f2c4e1a7b3d5f60", "event": "page.updated", "url": "https://example.com/hooks/grav", "request_headers": {"Content-Type": "application/json", "User-Agent": "Grav-Webhook/1.0", "X-Grav-Signature": "5d41402abc4b2a76b9719d911017c592...", "X-Grav-Event": "page.updated", "X-Grav-Delivery": "dlv_9f2c4e1a7b3d5f60"}, "request_body": {"event": "page.updated", "timestamp": "2026-09-21T12:00:00+00:00", "data": {"page": {"route": "/blog/hello", "title": "Hello", "slug": "hello"}}, "webhook_id": "wh_abc123"}, "created": 1790000000, "status_code": 200, "response_body": "ok", "duration_ms": 142, "success": true}], "meta": {"pagination": {"page": 1, "per_page": 20, "total": 1, "total_pages": 1}}, "links": {"self": "/api/v1/webhooks/wh_abc123/deliveries?page=1&per_page=20"}}
Response Codes
Test Webhook
/webhooks/{id}/test
Parameters
| Name | Type | Description |
|---|---|---|
| id required | string | Webhook id (path param). |
{"data": {"id": "dlv_1a2b3c4d5e6f7a8b", "event": "test", "url": "https://example.com/hooks/grav", "request_headers": {"Content-Type": "application/json", "User-Agent": "Grav-Webhook/1.0", "X-Grav-Signature": "5d41402abc4b2a76b9719d911017c592...", "X-Grav-Event": "test", "X-Grav-Delivery": "dlv_1a2b3c4d5e6f7a8b"}, "request_body": {"event": "test", "timestamp": "2026-09-21T12:00:00+00:00", "data": {"message": "This is a test webhook delivery."}, "webhook_id": "wh_abc123"}, "created": 1790000000, "status_code": 200, "response_body": "ok", "duration_ms": 118, "success": true}}