Part 1 of a 3-part deep dive on the VBWD webhooks engine. This part covers the architecture: the event bus, the outbound webhook system, and why the whole thing is event-driven by design. Part 2 covers signing, retries and safe delivery; Part 3 shows how VBWD's own plugins already use it.
Most platforms treat webhooks as an afterthought — a feature bolted on when a customer finally asks "can you notify my system when X happens?" VBWD treats them as a first-class consequence of an event-driven core. Because the platform already publishes domain events internally for everything that matters, exposing those events to the outside world is not a new subsystem to build; it's a relay to attach. This piece walks through how that relay actually works, with the real code.
At the centre of VBWD is a small, in-process event bus. When something meaningful happens — a payment is captured, a subscription activates, stock runs low — the responsible code publishes a named event with a plain-dictionary payload:
from vbwd.events.bus import event_bus
event_bus.publish("subscription.activated", {
"subscription_id": str(subscription.id),
"user_id": str(user_id),
"plan_slug": plan.slug,
"plan_name": plan.name,
})That's the entire contract for producing an event: a name and a dictionary. Any part of the system — core or plugin — can publish, and any number of subscribers can listen, including one special subscriber that listens to everything. That wildcard subscriber is where webhooks begin.
The outbound webhook system is deliberately agnostic — it never imports or names a single plugin. It attaches exactly one callback to the bus's wildcard (subscribe_all), and for every event that flows past, it asks a simple question: which registered endpoints wanted this event? For each match, it writes a delivery to a queue. Crucially, that enqueue is wrapped so that a webhook problem can never break the thing that emitted the event:
def _relay(event_name, data):
try:
service = OutboundWebhookService(...)
service.enqueue_for_event(event_name, data or {})
except Exception as relay_error: # never break event emission
logger.warning("[webhook] Relay failed for %s: %s", event_name, relay_error)This isolation is a design principle, not an accident. Your business logic publishes an event and moves on; whether a downstream webhook subscriber is slow, misconfigured, or on fire is entirely their problem, never yours.

A subtle but important decision: publishing an event enqueues deliveries synchronously, but it never performs the HTTP call inline. The moment an event fires, VBWD writes a delivery row with status pending — fast, transactional, local. A separate background scheduler then drains due deliveries on its own cadence (every 25 seconds, in batches), performing the actual signed HTTP POST. The event bus itself never touches the network.
This separation is what keeps the platform responsive. A checkout doesn't wait on someone else's server to accept a webhook; it records the intent to deliver and returns immediately. Delivery, with all its uncertainty and retries, happens out of band.
An outbound webhook in VBWD is a WebhookSubscription — an admin-registered endpoint stored in the vbwd_webhook table. Its shape is small and legible:
class WebhookSubscription(BaseModel):
__tablename__ = "vbwd_webhook"
url = db.Column(db.String(2048), nullable=False)
secret = db.Column(db.String(128), nullable=False)
event_types = db.Column(JSONB, nullable=False, default=list) # ["*"] means all
is_active = db.Column(db.Boolean, nullable=False, default=True)
description = db.Column(db.Text, nullable=True)
last_triggered_at = db.Column(db.DateTime, nullable=True)
consecutive_failure_count = db.Column(db.Integer, nullable=False, default=0)
def matches_event(self, event_name):
types = self.event_types or []
return "*" in types or event_name in typesTwo things are worth noticing. First, event_types is just a list of event names, and the sentinel ["*"] means "send me everything." A subscriber can be surgical (["payment.captured", "refund.reversed"]) or firehose (["*"]). Second, the model carries a consecutive_failure_count — the platform is keeping score, which becomes important for the self-healing behaviour we'll cover in Part 2.
VBWD keeps a small registry of subscribable event types purely to populate the admin's "which events?" dropdown. Core seeds the ones it publishes on the money path — payment.captured, payment.authorized, payment.refunded, payment.recurring_charge, refund.reversed and a handful more — plus the * wildcard and a synthetic webhook.test event for the admin's Test button.
But here's the elegant part: that registry is advisory. Delivery relays whatever event name is actually published on the bus, so an event that isn't in the dropdown still reaches any subscriber listening on ["*"]. The catalogue helps admins discover common events; it never limits what can flow. That's what lets plugins introduce their own events without core knowing anything about them — the subject of Part 3.
The payoff of building webhooks on top of an event bus rather than as a special case is that the platform gets integration for free. Every meaningful state change is already an event; making any of them available to an external system is a configuration action, not an engineering project. It's the difference between a platform where "notify my system when a subscription lapses" is a support ticket and one where it's a checkbox.
In Part 2 we'll open up the delivery half of the system: how each POST is signed with HMAC-SHA256, the exact headers your endpoint receives, how you verify them, and the retry-and-self-heal logic that decides what happens when your server has a bad day.
Curious whether an event-driven, self-hosted core fits what you're building? Request an enterprise installation and we'll walk your real workload through it.
Webhooks in VBWD — the series:Part 1: The engine (this part) · Part 2: Safe delivery · Part 3: Plugins on the bus