fucodo / webhook
Configurable incoming/outgoing webhooks with pluggable actions for Neos Flow
Installs: 11
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:neos-package
pkg:composer/fucodo/webhook
Requires
- php: >=8.1
- doctrine/orm: ^2.16
- guzzlehttp/guzzle: ^7.9
- neos/flow: ^8.0 || ^9.0
This package is auto-updated.
Last update: 2025-11-14 08:47:03 UTC
README
Configurable incoming/outgoing webhooks with pluggable actions for Neos Flow.
This package provides:
- Public incoming webhook endpoint(s) with HMAC verification.
- A small action system so you can plug behavior for each incoming webhook
(built-ins:
log,http-forward). - An HTTP client with timeouts, proxy, retry support.
- Outgoing webhook entity + sender service to notify external systems.
- CLI tooling to list/create/delete incoming webhooks and print ready-to-use cURL/Guzzle examples.
Requirements
- PHP >= 8.1
- Neos Flow ^8.0 or ^9.0
- Doctrine ORM ^2.16
- guzzlehttp/guzzle ^7.9
Installation
composer require fucodo/webhook
Flow detects the package by its package key Fucodo.Webhook.
Run migrations if prompted, then warm up caches:
./flow doctrine:migrate ./flow flow:cache:flush
Quick start
- Create a
WebhookActionConfiguration(persist an entity that selects an action and its options).- For a quick demo, you can create one for the built-in HTTP forwarder (
http-forward) pointing to a test URL.
- For a quick demo, you can create one for the built-in HTTP forwarder (
- Create an incoming hook (via CLI) and link it to the action configuration:
./flow incomingwebhook:create \ --path-segment github \ --action-config <actionConfigIdentifier>
- Call your endpoint:
- URL:
https://your-host/webhook/in/github - Default allowed methods:
POST(configurable) - Optional HMAC header if you set a secret:
X-Signature: sha256=<hmac>
The controller will verify the signature (if secret configured), dispatch the selected action with the JSON payload, and return a JSON result.
Incoming endpoint
- Route:
/webhook/in/{pathSegment} - Controller/action:
Fucodo.Webhook -> IncomingWebhookController::receiveAction() - Returns JSON body like:
{"success":true,"message":"...","data":{}}
- Status codes:
- 200 on success
- 400 on action failure
- 401 on signature verification failure
- 404 if webhook not found or disabled
- 405 if HTTP method not allowed
The pathSegment is the unique slug of your IncomingWebhook entity.
Signature verification (HMAC)
If your IncomingWebhook has a secret set, requests must include a header:
X-Signature: sha256=<hex-encoded-hmac>
where <hex-encoded-hmac> is hash_hmac('sha256', <raw-body>, <secret>).
Notes:
- Header name is case-insensitive;
X-Signatureorx-signatureboth work. - If no secret is configured, verification is skipped (see setting below).
Configuration (Settings.yaml)
Default settings live in Configuration/Settings.yaml of this package. You can
override them in your application settings.
Fucodo: Webhook: incoming: # Allowed HTTP methods if not set on entity level defaultAllowedMethods: ['POST'] # If true, allow requests without signature when secret is missing allowUnsignedWhenSecretMissing: true http: # total timeout (seconds) timeout: 5 # connection timeout (seconds) connectTimeout: 2 # TLS certificate verification verify: true # proxy string in Guzzle format, e.g. "http://user:pass@proxy:8080" proxy: null # number of retries on network / transient errors retries: 2 # delay between retries (milliseconds) retryDelayMs: 200
The HTTP settings are used by the internal HttpClient (Guzzle wrapper) for
http-forward and outgoing webhooks.
Routing
This package registers the following route by default:
- name: 'Webhook public incoming' uriPattern: 'webhook/in/{pathSegment}' defaults: '@package': 'Fucodo.Webhook' '@controller': 'IncomingWebhook' '@action': 'receive' '@format': 'html' appendExceedingArguments: true
You can prepend/override in your global Configuration/Routes.yaml if desired.
Domain model overview
-
IncomingWebhookpathSegment(unique slug used in the URL)enabled(boolean)secret(optional, for HMAC verification)allowedMethods(array, defaults to['POST'])staticHeaders(optional, reserved for future enhancements)actionConfiguration(ManyToOne toWebhookActionConfiguration)
-
WebhookActionConfiguration- Holds the selected action type and an
optionsarray consumed by the action.
- Holds the selected action type and an
-
OutgoingWebhooktargetUrl,httpMethod,headers(array)payloadTemplate(optional string template)enabled(boolean)- Optional
actionConfigurationto transform/build payload before sending - Optional
triggerEvent(free-form domain event name you can use in your app)
Action dispatching
Incoming requests are dispatched to the WebhookActionDispatcher with:
payload— JSON-decoded body (falls back to[]on invalid JSON)context— includesheadersand raw URLqueryoptions— taken fromWebhookActionConfiguration
Actions implement:
interface ActionInterface { public function execute(array $payload, array $context = [], array $options = []): ActionResultInterface; public static function identifier(): string; // machine name public static function label(): string; // human-readable label }
Return an ActionResult using the helpers:
ActionResult::ok(array $data = [], ?string $message = null) ActionResult::fail(?string $message = null, array $data = [])
Built-in actions
log(LogAction)- Options:
level(e.g.info,warning,error) - Logs the payload/context/options to the PSR logger.
- Options:
http-forward(HttpForwardAction)- Options:
url(required)method(default:POST)headers(array, merged withContent-Type: application/json)
- Sends the incoming JSON payload to the target URL using the configured HTTP client.
- Options:
Writing a custom action
Create a class implementing Fucodo\Webhook\WebhookAction\ActionInterface and register it as a Flow bean/service. Example skeleton:
<?php namespace Your\Package\WebhookAction; use Fucodo\Webhook\Domain\Model\ActionResult; use Fucodo\Webhook\Domain\Model\ActionResultInterface; use Fucodo\Webhook\WebhookAction\ActionInterface; use Neos\Flow\Annotations as Flow; /** * @Flow\Scope("singleton") */ class YourAction implements ActionInterface { public function execute(array $payload, array $context = [], array $options = []): ActionResultInterface { // ... do work return ActionResult::ok(['foo' => 'bar'], 'Done'); } public static function identifier(): string { return 'your-action'; } public static function label(): string { return 'Your custom action'; } }
Create a WebhookActionConfiguration that references your-action with its options.
CLI commands (incoming webhooks)
- List all hooks:
./flow incomingwebhook:list
- Show examples (cURL + Guzzle) for a hook:
./flow incomingwebhook:show --identifier <identifier>
- Create a hook:
./flow incomingwebhook:create \ --path-segment <slug> \ --action-config <actionConfigIdentifier>
- Delete a hook:
./flow incomingwebhook:delete --identifier <identifier>
Tip: If run via CLI, URL generation uses Neos.Flow.http.baseUri. Set it in your
settings so the shown URLs are absolute and correct.
Outgoing webhooks
Use Fucodo\Webhook\Service\OutgoingWebhookSender to send OutgoingWebhook
entities to external systems.
use Fucodo\Webhook\Domain\Model\OutgoingWebhook; use Fucodo\Webhook\Service\OutgoingWebhookSender; $hook = new OutgoingWebhook('https://example.com/endpoint'); $hook->setHeaders(['Content-Type' => 'application/json']); $hook->setHttpMethod('POST'); $result = $outgoingWebhookSender->send($hook, ['event' => 'order.created']); if (!$result['success']) { // handle error }
The sender returns an array with success, optional message, statusCode, and response.
It uses the same HttpClient and thus honors the HTTP settings and retries.
Examples (incoming)
cURL
BODY='{"hello":"world"}' SECRET='<your-secret-or-empty>' SIG="sha256=$(php -r "echo hash_hmac('sha256', file_get_contents('php://stdin'), getenv('SECRET'));" <<< "$BODY")" curl -i \ -X POST \ -H "Content-Type: application/json" \ -H "X-Signature: ${SIG}" \ --data "$BODY" \ https://your-host/webhook/in/<pathSegment>
PHP (Guzzle)
<?php $body = ['hello' => 'world']; $secret = '<your-secret>'; // optional $raw = json_encode($body); $headers = ['Content-Type' => 'application/json']; if ($secret !== null && $secret !== '') { $headers['X-Signature'] = 'sha256=' . hash_hmac('sha256', $raw, $secret); } $client = new \GuzzleHttp\Client(); $res = $client->request('POST', 'https://your-host/webhook/in/<pathSegment>', [ 'headers' => $headers, 'body' => $raw, ]);
Troubleshooting
- 401 "Signature verification failed":
- Ensure you send
X-Signature: sha256=<hmac>with HMAC computed on the exact raw request body. - Verify the secret matches the
IncomingWebhookentity.
- Ensure you send
- 405 "Method not allowed":
- Use a method listed in the hook's
allowedMethods(defaultPOST).
- Use a method listed in the hook's
- 404 "Webhook not found or disabled":
- Check the
pathSegmentandenabledflag.
- Check the
- HTTP forwarder errors:
- Inspect the message returned by
HttpForwardActionand the upstream status code. - Adjust
Fucodo.Webhook.http.*timeouts, proxy, and retry settings as needed.
- Inspect the message returned by
License
MIT. See LICENSE in the project root.