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

dev-main 2025-11-14 08:45 UTC

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

  1. 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.
  2. Create an incoming hook (via CLI) and link it to the action configuration:
./flow incomingwebhook:create \
  --path-segment github \
  --action-config <actionConfigIdentifier>
  1. 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-Signature or x-signature both 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

  • IncomingWebhook

    • pathSegment (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 to WebhookActionConfiguration)
  • WebhookActionConfiguration

    • Holds the selected action type and an options array consumed by the action.
  • OutgoingWebhook

    • targetUrl, httpMethod, headers (array)
    • payloadTemplate (optional string template)
    • enabled (boolean)
    • Optional actionConfiguration to 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 — includes headers and raw URL query
  • options — taken from WebhookActionConfiguration

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.
  • http-forward (HttpForwardAction)
    • Options:
      • url (required)
      • method (default: POST)
      • headers (array, merged with Content-Type: application/json)
    • Sends the incoming JSON payload to the target URL using the configured HTTP client.

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 IncomingWebhook entity.
  • 405 "Method not allowed":
    • Use a method listed in the hook's allowedMethods (default POST).
  • 404 "Webhook not found or disabled":
    • Check the pathSegment and enabled flag.
  • HTTP forwarder errors:
    • Inspect the message returned by HttpForwardAction and the upstream status code.
    • Adjust Fucodo.Webhook.http.* timeouts, proxy, and retry settings as needed.

License

MIT. See LICENSE in the project root.