Process Incoming Webhooks#
Incoming webhook processing is implemented with a Symfony event subscriber.
The Webhooks module handles the HTTP endpoint, signature or token verification,
and payload decoding. Your custom module subscribes to WebhookEvents::RECEIVE
and updates local Drupal data from the verified payload.
This example receives order status updates from an external system and creates
or updates a custom Drupal content entity type named acme_order.
Example payload#
The sender posts JSON to /webhook/acme_order_updates:
1 2 3 4 5 6 | |
In this example, the custom entity type has these fields:
| Field | Purpose |
|---|---|
remote_id |
Unique ID from the external system. |
name |
Entity label. |
remote_status |
Current status from the sender. |
remote_total |
Current total from the sender. |
Replace acme_order and the field names with your own entity type and fields.
Create the subscriber#
Create src/EventSubscriber/OrderWebhookSubscriber.php in your custom module:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
Queued retries
Calling $webhook->setStatus(FALSE) marks processing as failed. In
non-blocking mode the queue worker throws SuspendQueueException so the
item is retried on the next cron run instead of being deleted.
Register the subscriber#
Add the subscriber to acme_orders.services.yml. Use autowire: true so
Drupal resolves the constructor dependencies automatically by type hint:
1 2 3 4 5 6 | |
Clear caches after adding the service:
1 | |
Configure the incoming endpoint#
Create an incoming webhook configuration with:
| Setting | Value |
|---|---|
| Machine name | acme_order_updates |
| Type | Incoming |
| Content Type | application/json |
| Secret | Shared secret from the sender |
| Signature header | Header that carries the HMAC (default X-Hub-Signature-256, or X-WC-Webhook-Signature for WooCommerce) |
| Signature algorithm | Hash for raw HMAC values (default SHA-256) |
| Non-blocking | Checked for queued processing, unchecked for synchronous |
| Active | Checked |
The public endpoint becomes:
1 | |
Test the webhook#
Build a signed test request using the same secret stored on the incoming webhook config:
1 2 3 4 5 6 7 8 9 | |
A successful request returns 200 OK. If the webhook is non-blocking, run
cron before checking whether the acme_order entity was created or updated:
1 | |
Processing flow#
- The sender POSTs to
/webhook/acme_order_updates. WebhookControllercallswebhooks.service:receive().- The service loads the active incoming
webhook_configwith machine nameacme_order_updates. Returns404if none exists. - The posted body string is verified against the HMAC signature (WooCommerce:
trim(wp_json_encode($payload)); Hub: exact bytes). Returns401on mismatch. Do not parse and re-encode JSON for HMAC. - The body is decoded into a
Webhookvalue object. - The module dispatches
WebhookEvents::RECEIVEimmediately (blocking) or places the item in thewebhooks_dispatcherqueue (non-blocking). OrderWebhookSubscriber::onWebhookReceive()creates or updates the entity.