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.
- PHP >= 8.1
- Neos Flow ^8.0 or ^9.0
- Doctrine ORM ^2.16
- guzzlehttp/guzzle ^7.9
composer require fucodo/webhookFlow detects the package by its package key Fucodo.Webhook.
Run migrations if prompted, then warm up caches:
./flow doctrine:migrate
./flow flow:cache:flush- 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.
- 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.
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).
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: 200The HTTP settings are used by the internal HttpClient (Guzzle wrapper) for
http-forward and outgoing webhooks.
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: trueYou can prepend/override in your global Configuration/Routes.yaml if desired.
-
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)
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 = [])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:
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.
- 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.
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.
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
$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,
]);- 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
MIT. See LICENSE in the project root.