Skip to content

Webhook-Events

Plugins koennen Events emittieren, auf die Subscribers (externe Services wie Zapier, n8n, Make oder eigene Endpoints) reagieren. newmeta liefert ein vollstaendiges Webhook-System mit HMAC-Signatur, Queue-basiertem Delivery und Retry/Circuit-Breaker. Diese Seite zeigt, wie ein Plugin Events registriert und dispatcht.

Zwei Event-Typen

TypQuelleRegistrierung
AutoGeneriert aus content_construct=table-Pluginsautomatisch bei installWebhookEvents()
ManualEigene Business-Events, vom Plugin emittiert$this->webhookEvents im install()

Ein Plugin kann beides haben: Auto-Events fuer CRUD-Operationen auf seinen Tabellen, plus manuelle Events fuer Vorgaenge wie "Bestellung abgeschlossen" oder "Zertifikat ausgestellt".

Auto-Events (aus content_table)

Jedes Plugin mit content_construct=table bekommt drei Auto-Events pro Tabelle:

Event-NameTrigger
item.{table}.creatednach POST /api/backend/item
item.{table}.updatednach PATCH /api/backend/item
item.{table}.deletedvor DELETE /api/backend/item (Record noch verfuegbar)

Beispiel: Ein Plugin mit content_table = ["my_articles"] bekommt automatisch item.my_articles.created, item.my_articles.updated und item.my_articles.deleted — zero code.

Der Auto-Installer in install_controller.php::installWebhookEvents() liest alle Tabellen aus plugin_backend und legt die Events in webhook_events an:

php
// Auszug aus install_controller.php
$pbQ = query("SELECT content_table FROM plugin_backend
              WHERE plugin_id = $pluginID
                AND content_construct = 'table'
                AND content_table != ''");
while ($row = fetch_assoc($pbQ)) {
    $table = real_escape_string($row['content_table']);
    foreach (['created', 'updated', 'deleted'] as $op) {
        $eventName = "item.$table.$op";
        query("INSERT IGNORE INTO webhook_events
               (plugin_id, event_name, event_type, content_table, description)
               VALUES ($pluginID, '$eventName', 'auto', '$table', '...')");
    }
}

Manuelle Events registrieren

Fuer Business-Events, die nicht durch CRUD ausgeloest werden, befuelle $this->webhookEvents im install():

php
public function install()
{
    // ... andere Properties

    $this->webhookEvents = [
        '{
            "name":        "blog.published",
            "type":        "manual",
            "description": "Blog-Artikel veroeffentlicht"
        }',
        '{
            "name":        "blog.unpublished",
            "type":        "manual",
            "description": "Blog-Artikel zurueckgezogen"
        }'
    ];
}

Jeder Eintrag ist ein JSON-String mit name, type (konventionell "manual") und optionaler description. Das install_controller::installWebhookEvents() nimmt INSERT IGNORE — doppeltes Install ist safe.

Event dispatchen

Im Plugin-Code (API-Model, Script, Controller):

php
require_once $_SERVER['DOCUMENT_ROOT'] . '/_core/system/webhook/WebhookDispatcher.php';

WebhookDispatcher::dispatch('blog.published', [
    'post_id'      => $postId,
    'title'        => $title,
    'author_id'    => $authorId,
    'published_at' => date('c'),
]);

dispatch($eventName, $data) ist non-blocking — es schreibt einen Eintrag in webhook_queue und kehrt sofort zurueck. Der WebhookDeliveryWorker picked den Job per console/bin process-webhooks auf und macht den HTTP-POST.

Sensitive-Data-Filter

Vor dem Queue-Insert filtert der Dispatcher automatisch sensible Keys aus dem Payload — rekursiv, case-insensitive:

password, secret, hash, key, token, salt

Beispiel: ['email' => '[email protected]', 'password' => 'p4ss', 'settings' => ['api_key' => 'xyz']] wird zu ['email' => '[email protected]', 'settings' => []]. Das schuetzt vor versehentlichem Leaken von Credentials in Webhooks.

Zusaetzliche Keys koennen in WebhookDispatcher::SENSITIVE_KEYWORDS ergaenzt werden.

HMAC-Signatur

Der Worker signiert jeden Payload mit einem HMAC-SHA256-Hash und dem Subscription-Secret:

php
$signature = hash_hmac('sha256', $payload, $subscription->secret);

Der Receiver validiert:

php
$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected  = hash_hmac('sha256', $payload, 'your_webhook_secret');
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    die('Invalid signature');
}
$data = json_decode($payload, true);

Das Secret wird bei der Subscription-Erstellung im Backend (/admin/api-keys → Webhook-Section) einmal generiert und angezeigt — danach nicht wieder abrufbar. Rotation: neuer Secret via "Rotate"-Button, alter wird sofort invalidiert.

Delivery-Headers

Jeder ausgehende HTTP-POST hat folgende Header:

Content-Type: application/json
User-Agent: newmeta-CMS/3.0 Webhook
X-Webhook-Event: {event_name}
X-Webhook-Signature: {hmac_sha256}
X-Delivery-ID: {queue_id}

Zusaetzlich: Custom-Headers aus webhook_subscriptions.custom_headers (JSON key:value) werden ans Ende gemerged.

Retry + Circuit Breaker

Bei 4xx/5xx-Response oder Connection-Error:

  • Retry mit Exponential Backoff: nach dem initialen Fehlversuch folgen bis zu 5 Retries mit Wartezeit 30s → 2min → 8min → 32min → 2h (also insgesamt bis zu 6 Delivery-Attempts). Default aus webhook_subscriptions.max_retries = 5, pro Subscription anpassbar (im Code: (int)($job['max_retries'] ?: 5)).
  • Circuit Breaker: Nach 10 aufeinanderfolgenden Fehlschlaegen wird die Subscription automatisch deaktiviert (active = 0) und disabled_reason gesetzt. Admin muss manuell reaktivieren.
  • Cleanup: Erfolgreich zugestellte Queue-Items werden nach 24h automatisch geloescht.

Worker starten

Der WebhookDeliveryWorker laeuft ueber die CLI:

bash
php console/bin process-webhooks --time-limit=25

Empfohlener Cron-Setup fuer 30s-Taktung:

cron
* * * * * php /pfad/console/bin process-webhooks --time-limit=25
* * * * * sleep 30 && php /pfad/console/bin process-webhooks --time-limit=25

Details zum Scheduling: Scheduled Tasks.

Event-Matching (Wildcards)

Subscriptions koennen drei Matching-Modi nutzen:

Subscription-EventsMatched
"blog.published"nur blog.published
"item.*"alle Auto-Events (item.blog.created, item.users.deleted, …)
"*"alle Events systemweit

Das ermoeglicht z. B. Audit-Subscriptions, die alles loggen, ohne bei jedem neuen Plugin manuell erweitert werden zu muessen.

System-Events (nicht plugin-gebunden)

Der Core dispatched 11 System-Events, die kein Plugin registrieren muss:

EventZeitpunkt
page.publishedNach Pagebuilder-Publish
page.draft_discardedDraft verworfen
media.uploadedMedia-Upload erfolgreich
media.deletedMedia-Datei geloescht
auth.loginBackend-Login
auth.logoutBackend-Logout
order.createdShop-Order (Stripe + PayPal dispatchen separat)
form.submittedForm-Submit ueber /api/form/submit
user.registeredNeuer Frontend-User registriert
emailmarketing.subscribedNewsletter-Subscribe
deployment.completedNach Deploy-Target-Run

Datenbank-Tabellen

TabelleZweck
webhook_eventsEvent-Registry (pro Plugin, auto/manual)
webhook_subscriptionsUser-erstellte Abonnements (Name, URL, Secret, Event-Liste, Custom-Headers, max_retries, failure_count, disabled_reason)
webhook_queuePending/Retry/Delivered-Queue
webhook_deliveriesLog (Request-Payload, Response-Code, Duration)

Schema-Migration: _migrations/core/010_create_webhooks.sql. Details in Migrations.

Management-UI

  • Subscriptions anlegen: /admin/api-keys → Tab "Webhooks" → Event-Picker (grouped Multi-Select mit "Select All" pro Plugin), HTTPS-URL, Secret-Auto-Generator, Test-Ping-Button
  • Delivery-Historie: /admin/task-manager → Tab "Webhooks" → Queue + Stats (Success-Rate)

Testing-Tipps

  • Test-Ping: Im Webhook-Management-Panel sendet Test-Ping einen Dummy-Event an die Subscription-URL. Ideal fuer Initial-Setup.
  • ngrok fuer lokale Receiver: ngrok http 3001 und die ngrok-URL als Subscription-URL eintragen — HTTPS wird erzwungen, das passt.
  • Queue-Inspektion: SELECT * FROM webhook_queue WHERE status IN ('pending', 'retry') ORDER BY next_retry_at. retry_count und error_message zeigen, warum ein Delivery haengt.

Haeufige Fehler

dispatch() ohne require_once

WebhookDispatcher ist kein Autoloader-Teil — jeder Call muss vorher die Datei inkludieren: require_once $_SERVER['DOCUMENT_ROOT'] . '/_core/system/webhook/WebhookDispatcher.php'. Sonst Fatal error: Class 'WebhookDispatcher' not found.

HTTP statt HTTPS als Subscription-URL

Die Subscription-Verwaltung zwingt https://. Fuer lokales Testing auf http:// ausweichen (ngrok liefert HTTPS) — nie produktive Webhooks auf HTTP, weil die Signatur-Validierung gegen MITM nichts nuetzt.

Receiver antwortet langsam → Timeouts

Der Worker hat 10s cURL-Timeout. Receiver, die laenger brauchen (synchrone DB-Queries, externe API-Calls), verursachen Retries. Empfehlung: Receiver antwortet sofort mit 200, macht die Arbeit asynchron (Queue oder Background-Job).

INSERT IGNORE — doppelte Events erkennen

installWebhookEvents() nutzt INSERT IGNORE, Events mit demselben (plugin_id, event_name) werden stumm ignoriert. Wenn eine description aktualisiert werden soll, muss das Plugin in update() einen UPDATE webhook_events-Call machen — ein erneutes install() aendert nichts.

Sensitive-Keyword-Filter matcht zu aggressiv

Der Filter matcht jeden Key, der einen der Keywords enthaelt (case-insensitive substring). api_key, key, private_key werden gefiltert — auch harmlose Felder wie category_key oder key_user_id sind weg. Das ist bewusst konservativ. Bei Bedarf: sensible Daten in flachen, nicht-keyword-matched Sub-Keys (z. B. public_identifier) halten.

Siehe auch

  • Plugin-Anatomie$this->webhookEvents-Property
  • API-Endpunkte — Events aus Endpoints dispatchen
  • Scheduled Tasksprocess-webhooks-Worker per Cron
  • Migrations_migrations/core/010_create_webhooks.sql
  • Webhook-Management-Panel: /admin/api-keys → Tab "Webhooks"
  • Delivery-Historie: /admin/task-manager → Tab "Webhooks"