Developer Guide#
This guide covers the integration APIs available in the Webhooks module for
custom module authors: the service layer, event system, payload objects,
signature handling, queue processing, Drush commands, and the optional
webhook submodule.
Architecture overview#
flowchart LR
subgraph outgoing [Outgoing flow]
HookImpl["Hook / custom code"] --> WS["webhooks.service\n(WebhooksService)"]
WS -->|"dispatch SendEvent"| ED["Event Dispatcher"]
WS -->|"HTTP POST"| Remote["Remote endpoint"]
WS -->|"on error dispatch"| EDErr["SendErrorEvent"]
end
subgraph incoming [Incoming flow]
Remote2["Remote system"] -->|"POST /webhook/{name}"| Controller["WebhookController"]
Controller --> WS2["webhooks.service\n(receive)"]
WS2 -->|"verify signature/token"| WS2
WS2 -->|"blocking: dispatch ReceiveEvent"| ED2["Event Dispatcher"]
WS2 -->|"non-blocking: enqueue"| Queue["webhooks_dispatcher queue"]
Queue -->|"cron"| QW["WebhookDispatcher\nQueueWorker"]
QW -->|"dispatch ReceiveEvent"| ED2
end
The webhooks.service service#
The primary API surface is the webhooks.service service, registered in
webhooks.services.yml. It implements three interfaces:
| Interface | Responsibility |
|---|---|
WebhookDispatcherInterface |
Load configs by event, trigger all subscribers, send a single webhook |
WebhookReceiverInterface |
Receive and verify an inbound webhook request |
WebhookSerializerInterface |
Encode/decode payloads via Symfony Serializer |
Injecting the service#
1 2 3 4 5 6 7 8 9 | |
Key methods#
loadMultipleByEvent(string $event, string $type = 'outgoing'): WebhookConfigInterface[]#
Returns all active webhook_config entities subscribed to $event.
1 | |
triggerEvent(Webhook $webhook, string $event): void#
Loads every active outgoing config for $event and calls send() for each.
1 2 3 4 5 6 7 8 9 | |
send(WebhookConfig $webhookConfig, Webhook $webhook): void#
Sends a single webhook to the URL defined in $webhookConfig. Dispatches
WebhookEvents::SEND before the request and WebhookEvents::SEND_ERROR if
the HTTP call throws an exception.
receive(string $name): Webhook#
Validates the incoming request against the active webhook_config entity with
machine name $name, verifies any signature or token, and either dispatches
WebhookEvents::RECEIVE immediately or enqueues the payload for later
processing. If verification fails, the service logs the failure (payload
length only, not the body), dispatches WebhookEvents::RECEIVE_ERROR, and
rethrows; RECEIVE is not dispatched.
A WooCommerce activation ping (webhook_id= plus digits, no
X-WC-Webhook-* headers) returns 200 without HMAC verification and
without dispatching RECEIVE. Every other request is still verified.
The Webhook value object#
Drupal\webhooks\Webhook is the data transfer object passed between all parts
of the system.
Constructor#
1 2 3 4 5 6 | |
Automatic headers#
Certain setters automatically populate the headers array:
| Setter | Header set |
|---|---|
setEvent(string $event) |
X-Drupal-Event |
setUuid(string $uuid) |
X-Drupal-Delivery |
setContentType(string $ct) |
Content-Type |
setSignature(string $body) |
X-Hub-Signature (SHA-1) and X-Hub-Signature-256 (SHA-256) |
Payload manipulation#
Use setPayload() to replace the entire payload array, or addPayload() to
merge additional top-level keys into the existing payload:
1 2 3 4 5 | |
Both methods are available on the Webhook object at any point, but the
right place to modify an outgoing payload is inside a WebhookEvents::SEND
subscriber — see Modifying outgoing payloads
below.
Incoming requests also keep the posted bytes on getBody() /
setBody(). HMAC verification uses that string, not
json_encode(getPayload()). Compare SHA-256 or byte length of
getBody() / the stored entity payload field to the HTTP body;
do not compare pretty-printed JSON.
Signature generation and verification#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Events#
The module dispatches three Symfony events defined in WebhookEvents.
WebhookEvents::SEND (webhook.send)#
Dispatched immediately before an outgoing HTTP request is made. Use this to inspect or modify the webhook before it is sent.
WebhookEvents::SEND fires after the Webhook object is constructed but
before the payload is encoded and before the HMAC signature is calculated.
Changes made inside this event are reflected in the signed body that reaches
the remote endpoint.
Event class: Drupal\webhooks\Event\SendEvent
Available methods:
| Method | Returns |
|---|---|
$event->getWebhook() |
The mutable Webhook value object |
$event->getWebhookConfig() |
The WebhookConfig config entity |
Modifying outgoing payloads#
The subscriber below shows the recommended pattern: check the event or config
to limit changes to the webhooks you intend to affect, then use addPayload()
to merge extra keys or setPayload() to replace the payload entirely.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
Including referenced entities in the payload#
By default, the entity hooks normalize entity reference fields as lightweight
stubs — only the target entity's id and type are included. If the
receiving system needs the full referenced entity data, load and normalize the
references inside a WebhookEvents::SEND subscriber and attach them under an
included key, following a pattern similar to JSON:API.
This approach keeps the original entity shape intact and avoids recursion problems when referenced entities also have references.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
Register the subscriber in your module's services.yml:
1 2 3 4 5 6 7 | |
Payload size and recursion
Loading and normalizing many referenced entities can produce large payloads and slow the request. Consider limiting expansion to specific fields or bundles, and be mindful of circular references when referenced entities themselves contain entity reference fields.
WebhookEvents::RECEIVE (webhook.receive)#
Dispatched when an inbound webhook passes all verification checks. Use this to act on the received payload.
Event class: Drupal\webhooks\Event\ReceiveEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
For a full walkthrough, see Process Incoming Webhooks.
WebhookEvents::RECEIVE_ERROR (webhook.receive.error)#
Dispatched when inbound signature or token verification fails. Listeners
may log the request for debugging. Do not process the payload here: the
HTTP response remains 401 and WebhookEvents::RECEIVE is not dispatched.
Unknown endpoints (404) do not fire this event.
Event class: Drupal\webhooks\Event\ReceiveErrorEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
WebhookEvents::SEND_ERROR (webhook.send.error)#
Dispatched when an outgoing webhook HTTP request throws an exception.
Event class: Drupal\webhooks\Event\SendErrorEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Registering an event subscriber#
Declare the subscriber as a tagged service. Use autowire: true so Drupal
resolves constructor dependencies by type hint:
1 2 3 4 5 6 7 | |
Altering available outgoing event options#
Modules can add custom events to the event selection table in the
webhook_config add/edit form by implementing
hook_webhooks_event_info_alter().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Trigger a custom event from anywhere using triggerEvent():
1 2 3 4 5 6 7 8 | |
Queue processing#
Non-blocking incoming webhooks are stored in the webhooks_dispatcher queue.
The WebhookDispatcher queue worker plugin processes items during cron,
spending up to 60 seconds per cron invocation.
If the WebhookEvents::RECEIVE listener calls
$event->getWebhook()->setStatus(FALSE), the queue worker throws
SuspendQueueException so the item is retried on the next cron run rather than
deleted.
Reliable vs. best-effort queues#
The queue backend is chosen at service instantiation time based on the
webhooks.settings:reliable configuration value.
| Setting | Queue class | Guarantee |
|---|---|---|
reliable: false (default) |
Default queue | Best-effort; order may not be preserved |
reliable: true |
Reliable queue | At-least-once; order preserved |
Note
Changing this setting only affects new queue items; existing items continue with the previously configured backend until they are processed.
Queue item structure#
Each queue item is an associative array:
1 2 3 4 | |
Drush commands#
The module provides two Drush commands registered in drush.services.yml.
webhooks:trigger (alias: wt)#
Manually trigger all active outgoing webhooks subscribed to a given event. Useful for testing or replaying events.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Options:
| Option | Default | Description |
|---|---|---|
--payload |
'' |
JSON-encoded payload body |
--headers |
'' |
JSON-encoded additional headers |
--content_type |
application/json |
MIME type for the request body |
webhooks:list (alias: wt)#
List all webhook configurations with their type and status.
1 2 3 4 5 6 7 8 | |
Output fields: Name, Machine Name, Type, Status
The webhook submodule#
The optional webhook submodule (modules/webhook/) listens to
WebhookEvents::RECEIVE and WebhookEvents::RECEIVE_ERROR and persists each
inbound webhook as a webhook content entity with the following base fields:
| Field | Type | Description |
|---|---|---|
title |
String | "Webhook {uuid}" |
webhook_config |
String | Incoming webhook_config machine name |
headers |
String (long) | JSON-encoded request headers |
payload |
String (long) | Posted request body (not re-encoded JSON) |
payload_length |
Integer | Byte length of payload |
signature |
String | Value of the configured HMAC header |
verified |
Boolean | TRUE when verification passed; FALSE on signature or token failure |
error |
String (long) | Verification exception message when verified is FALSE |
created |
Timestamp | Unix timestamp of receipt |
Failed verification still returns 401. Records are for debugging only;
business processing must subscribe to RECEIVE, not RECEIVE_ERROR.
The webhooks log channel records the mismatch (with payload length, not
the body) and, when this submodule is enabled, a notice with a View
link to the stored entity.
Records are viewable at /admin/content/webhook (requires the
access webhook overview permission) and at /webhook/{id}.
modules/webhook/src/EventSubscriber/WebhookSubscriber.php is a practical
reference implementation of RECEIVE and RECEIVE_ERROR subscribers.
Replay a stored delivery with the entity payload, signature, and the
incoming webhook_config secret:
1 2 3 4 5 6 | |
Config entity reference#
webhook_config entities are exportable configuration with the file prefix
webhooks.webhook.*. The full list of exported properties:
| Property | Type | Description |
|---|---|---|
id |
string | Machine name |
label |
string | Human-readable label |
type |
string | incoming or outgoing |
payload_url |
string | Target URL (outgoing only) |
events |
array | Subscribed event machine names (outgoing only) |
content_type |
string | MIME type for serialization |
secret |
string | HMAC secret |
signature_header |
string | HTTP header that carries the HMAC |
signature_algorithm |
string | HMAC algorithm for unprefixed (base64) signatures |
token |
string | Legacy token |
non_blocking |
bool | Queue inbound payloads instead of processing synchronously |
Example YAML#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Exceptions#
| Class | Thrown when |
|---|---|
WebhookMismatchSignatureException |
HMAC signature on an incoming request does not match, or a signature was sent with no secret configured. The message does not include the payload. |
WebhookMismatchTokenException |
Token on an incoming request does not match |
WebhookIncomingEndpointNotFoundException |
No active incoming webhook_config entity matches the URL machine name |