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 leveldefaultAllowedMethods: ['POST']# If true, allow requests without signature when secret is missingallowUnsignedWhenSecretMissing: truehttp:
# total timeout (seconds)timeout: 5# connection timeout (seconds)connectTimeout: 2# TLS certificate verificationverify: true# proxy string in Guzzle format, e.g. "http://user:pass@proxy:8080"proxy: null# number of retries on network / transient errorsretries: 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 {
publicfunctionexecute(array$payload, array$context = [], array$options = []): ActionResultInterface;
publicstaticfunctionidentifier(): string; // machine namepublicstaticfunctionlabel(): 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:
<?phpnamespaceYour\Package\WebhookAction;
useFucodo\Webhook\Domain\Model\ActionResult;
useFucodo\Webhook\Domain\Model\ActionResultInterface;
useFucodo\Webhook\WebhookAction\ActionInterface;
useNeos\Flow\AnnotationsasFlow;
/** * @Flow\Scope("singleton") */class YourAction implements ActionInterface
{
publicfunctionexecute(array$payload, array$context = [], array$options = []): ActionResultInterface
{
// ... do workreturn ActionResult::ok(['foo' => 'bar'], 'Done');
}
publicstaticfunctionidentifier(): string { return'your-action'; }
publicstaticfunctionlabel(): 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.
useFucodo\Webhook\Domain\Model\OutgoingWebhook;
useFucodo\Webhook\Service\OutgoingWebhookSender;
$hook = newOutgoingWebhook('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.