Part 3 of a 3-part deep dive on the VBWD webhooks engine. Part 1 covered the event bus and relay; Part 2 covered signing and delivery. This part is the practical one: how VBWD's own plugins already publish events, how a plugin adds its own event types, and how you register and test a webhook.
The strongest argument for an event-driven core is that its own features use it. In VBWD, webhooks aren't a demo waiting for a customer — they're the same bus the platform's plugins already publish to every day. If you subscribe to ["*"] on a running instance with the standard plugins enabled, events are already flowing.
Because publishing is just event_bus.publish(name, data), plugins emit domain events as a matter of course. A few real examples from the shipped plugin set:
subscription.activated, subscription.cancelled, subscription.expired, addon.activated, addon.cancelledinvoice.paid / invoice.refunded eventsThe subscription plugin is a clean illustration. It centralises its lifecycle publishes in one place so every emit site — the payment path, admin routes, the expiry scheduler — produces the same stable payload:
# plugins/subscription — one home for lifecycle events
EVENT_SUBSCRIPTION_ACTIVATED = "subscription.activated"
EVENT_SUBSCRIPTION_CANCELLED = "subscription.cancelled"
EVENT_SUBSCRIPTION_EXPIRED = "subscription.expired"
def publish_subscription_event(event_name, subscription, user_id):
from vbwd.events.bus import event_bus
plan = subscription.tarif_plan
event_bus.publish(event_name, {
"subscription_id": str(subscription.id),
"user_id": str(user_id),
"plan_id": str(plan.id) if plan else None,
"plan_slug": plan.slug if plan else None,
"plan_name": plan.name if plan else None,
})None of these plugins know that webhooks exist. They publish domain events because that's how the platform is wired; the outbound relay from Part 1 turns those same events into external deliveries for anyone who subscribed. That's the whole point of an agnostic core — the producer and the integration never have to know about each other.
Core seeds the common payment events into the admin's event-type dropdown, but a plugin can add its own so admins can discover and subscribe to them by name. It does this from its on_enable, calling a single core function — and, importantly, core never has to name the plugin back:
from vbwd.webhooks.event_types import register_webhook_event_type
class SubscriptionPlugin(BasePlugin):
def on_enable(self):
register_webhook_event_type("subscription.activated", "Subscription activated")
register_webhook_event_type("subscription.cancelled", "Subscription cancelled")
register_webhook_event_type("subscription.expired", "Subscription expired")Remember from Part 1 that this registry is advisory. Registering an event type only makes it appear in the admin dropdown; even an unregistered event still reaches any ["*"] subscriber, because delivery relays whatever is published. So a plugin gets webhook support with zero webhook code — publishing the event is enough; registering the type is just a courtesy to admins.
On the operator side, webhook subscriptions are managed through a small admin REST surface under /api/v1/admin/webhooks:
GET /admin/webhooks — list subscriptionsGET /admin/webhooks/event-types — the subscribable event cataloguePOST /admin/webhooks — create a subscriptionPUT /admin/webhooks/<id> — update url / events / descriptionPOST /admin/webhooks/<id>/toggle — enable or disablePOST /admin/webhooks/<id>/test — send a synthetic webhook.test deliveryDELETE /admin/webhooks/<id> — remove itCreating one is a single call. You supply a URL and the events you care about; VBWD generates a strong signing secret for you:
POST /api/v1/admin/webhooks
{
"url": "https://your-app.example.com/hooks/vbwd",
"events": ["subscription.activated", "payment.captured"],
"description": "Sync activations into our CRM"
}
# → { "id": "...", "secret": "whsec_…", "status": "active", ... }Then hit the /test endpoint (or the admin's Test button, which emits the synthetic webhook.test event) and watch a real signed delivery arrive at your endpoint, complete with the headers from Part 2. Every delivery is recorded, so you can inspect the exact request and response for each attempt while you're wiring things up.
Put the three parts together and the shape is clear. VBWD is event-driven at the core; the outbound webhook system is a single agnostic relay on the bus; delivery is signed, retried and self-healing; and the plugins already publish the events, so subscribing to them is configuration, not development. The result is that "notify my system when X happens" — the request that turns into a project on most platforms — is a checkbox here, for any event any plugin publishes, including ones written after the core shipped.
It's also entirely yours. Because VBWD is self-hosted and source-available, the bus, the relay, the deliveries and the secrets all live inside your own perimeter — no third party sits between your events and your systems. And because it's written in tested Python with documentation structured for both engineers and LLM coding agents, extending it — adding an event, wiring a new integration — is the kind of change a small team, or an agent working alongside them, can ship quickly.
If you're evaluating an event-driven, self-hosted foundation for a commercial platform — one where webhooks, plugins and an auditable core come as standard rather than as add-ons — request an enterprise installation and we'll run your real events through it.
Webhooks in VBWD — the series:Part 1: The engine · Part 2: Safe delivery · Part 3: Plugins on the bus (this part)