Your Billing System Already Knows. Your CRM Doesn’t. Here’s the Wire.

Every subscription event your platform fires — a signup, a renewal, a cancellation — could be lighting up your CRM, Slack, or n8n in real time. Here’s how VBWD’s signed outbound webhooks do exactly that.

Rewritten for redrobot.

Picture the gap between two facts. Your billing platform knows, to the second, that a customer just renewed. Your CRM, three tools over, has no idea. Somewhere in that gap a human is pasting rows into a spreadsheet, or a cron job is limping along six hours behind reality, quietly deciding who gets a welcome email and who gets forgotten.

The interesting thing is that the information already exists. It just has no way out. Closing that gap is exactly what outbound webhooks do in VBWD, and the mechanism is simpler and more general than most integration layers you have wired up before. This piece walks through what actually fires, how the platform proves a delivery is real, what you can subscribe to, and the failure modes nobody warns you about until they burn you.

One relay, subscribed to everything

Most platforms make you pick your webhook events from a checklist, one type at a time. VBWD takes a different route, and it starts with how the platform is built internally.

At the center of VBWD is an event bus. Every meaningful thing a service does — a subscription converting from trial to paid, an invoice getting marked paid, a payment captured, a user registering — is published as a domain event onto that bus. Core subscribers and plugins listen and react. That is the mechanism that keeps the platform modular instead of a tangle of direct calls. The architecture overview explains how the agnostic core and its plugin layer keep those seams clean.

The outbound webhook relay is just one more listener on that bus — with one twist. Instead of subscribing to a single named event, it does a subscribe-all. Every event that flows through the system is a candidate for delivery. You never wire event types individually in code. You register a target URL through the admin API, and the relay fans matching events out to it.

The consequence is worth sitting with. Your integration surface is broad by default and grows on its own. Install a plugin that publishes a new event type, and your existing webhook starts receiving it with no platform change on your side. Since VBWD is self-hosted and source-available — Python, Flask, PostgreSQL, Vue 3, Docker — the bus and the relay both run inside your own infrastructure, not a vendor’s cloud.

Registering endpoints, and why the signature is the whole point

Endpoints live behind ordinary admin CRUD at /admin/webhooks. Create one by handing it a target URL; list, update, and delete them like any other admin resource. Each webhook is a stored record — target URL, a signing secret, delivery configuration — persisted in Postgres alongside the rest of your platform state.

Here is the part that separates working integrations from ones that quietly leak. Every delivery is HMAC-signed. When the relay POSTs a payload, it computes an HMAC over the request body using that webhook’s shared secret and attaches the result as a header.

Treat that signature as the entire security model, because it is. Your endpoint URL will not stay secret — over months it drifts into logs, proxy configs, and browser histories. Anyone who learns it can POST fabricated events straight at your receiver. The HMAC is the only thing that lets your side tell a genuine VBWD delivery from a forgery. Verify it before you trust a single field.

A delivered payload is a domain event wrapped in a thin envelope:

{
  "event": "subscription.renewed",
  "id": "evt_9f2c1a7b",
  "created_at": "2026-07-15T09:14:03Z",
  "data": {
    "subscription_id": 4821,
    "user_id": 1337,
    "tarif_plan_slug": "pro-monthly",
    "status": "active",
    "period_end": "2026-08-15T09:14:03Z"
  }
}

Exact fields vary by event type and by the plugins that produced them, but the shape holds: a name you branch on, a stable id you deduplicate against, a timestamp, and a data object.

Verifying on the receiving end

Do this before any parsing or logic. Read the raw body as bytes, recompute the HMAC with your shared secret, and compare using a constant-time check so you leak no timing information. A minimal Python receiver:

import hmac, hashlib
from flask import Flask, request, abort

WEBHOOK_SECRET = b"your-shared-secret"

app = Flask(__name__)

@app.post("/vbwd-hook")
def receive():
    raw_body = request.get_data()  # exact bytes, not re-serialized
    sent_sig = request.headers.get("X-VBWD-Signature", "")

    expected = hmac.new(
        WEBHOOK_SECRET, raw_body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, sent_sig):
        abort(401)  # forged or corrupted; drop it

    event = request.get_json()
    handle_event(event)  # only now do you trust it
    return "", 200

Two traps catch people repeatedly. First, sign over the raw bytes. Parse the JSON and re-serialize it before hashing, and a difference in key order or whitespace will make a valid signature fail verification. Second, keep the secret out of source control — it belongs in an environment variable or a secrets manager, next to your database credentials. Confirm the exact header name your build sends against the integration docs, since the header key is part of the delivery contract.

What you can listen for

Because the relay subscribes to everything, the catalogue is the union of every event the core and your enabled plugins publish. The buckets you will reach for most:

  • Subscription lifecycle — created, trial started, trial-to-paid conversion, renewed, past-due, cancelled, expired. The richest vein for CRM updates and dunning flows.
  • Invoicing — invoice created, invoice paid, payment captured. Feeds accounting sync and receipt automations.
  • User events — registration and account changes, for onboarding and welcome sequences.
  • Plugin-published events — whatever a plugin you installed emits. A shop, booking, or marketplace plugin adds its own events to the stream and your webhook picks them up automatically.

The practical rule: branch on the event field and ignore what you do not need. Do not assume the set is frozen — treat an unknown event name as a no-op rather than an error, so enabling a new plugin can never break your integration. The plugin catalogue shows which plugins add which event streams.

A worked example: renewals into your CRM with n8n

Say every subscription renewal should update a deal record and drop a note in a team channel. In n8n — the same shape works with a Zapier catch hook:

  • Endpoint. Add a Webhook node set to POST. It hands you a URL. Copy it.
  • Register. In the admin webhooks CRUD at /admin/webhooks, create a webhook with that URL as the target and store the signing secret n8n will use.
  • Verify. A Function node right after the Webhook node recomputes the HMAC over the raw body and compares. Fail closed. Same check as the Python snippet, in JavaScript.
  • Filter. A Switch node routes on event. Only subscription.renewed continues; the rest exit quietly.
  • Deduplicate. Before writing anything, check whether you have already handled this id. Static data or a small lookup table does the job. Not optional — see below.
  • Act. One branch updates the CRM deal by user_id; another posts to Slack. Both read from the verified data.

That is roughly fifteen minutes of node wiring and zero VBWD code changes. The relay already publishes every renewal, so you are subscribing to a stream that exists rather than building one. VBWD’s feature set lists the domain areas that emit events you can route this way.

The failure modes nobody mentions upfront

Webhooks are quick to start and easy to get subtly wrong. Plan for these:

  • Verification is your job, not the platform’s. VBWD signs every delivery, but nothing forces your receiver to check. An endpoint that skips the HMAC is an open door with a welcome mat.
  • Deliveries can repeat. The relay retries. If your endpoint is slow, times out, or returns a non-2xx even after doing the work, the same event comes back. Key your processing on the event id and make repeat handling a no-op. “Received a webhook” never means “this happened exactly once.”
  • Retries are bounded, not infinite. If your receiver is down, the relay retries per its policy and logs each attempt, so a brief hiccup loses nothing. But redelivery only happens inside the retry window. Outlast it and you need a reconciliation path — a periodic pull to backfill — because a webhook stream is a fast notification layer, not a durable queue you can ignore for a day.
  • Delivery logs are your first debugging move. The relay records what it sent and how your endpoint responded. Read them before assuming the payload was wrong.
  • Plugin-side reactions must commit their own session. If you extend VBWD with a plugin that reacts to the same events and writes to the database inside an event-bus callback, that callback has to commit its own session. A handler that mutates data and forgets to commit silently does nothing — the classic “the event fired but the record never changed” puzzle, on the platform side.

The short version

Outbound webhooks turn VBWD’s internal event bus into an integration surface you control. Any subscription lifecycle event or other domain event can fire an HMAC-signed POST to a URL you register through admin CRUD, and a subscribe-all relay handles delivery with retries and keeps logs. Your half of the contract is small but firm: verify the signature, deduplicate on the event id, and keep a reconciliation fallback for outages longer than the retry window. Do those three and you can wire renewals into a CRM, payments into accounting, and signups into onboarding — no nightly sync, no copy-paste.

And because VBWD is self-hosted and source-available — BSL 1.1, free for commercial use while annual VBWD-attributable sales stay under the value of 6.7 BTC — the relay, the bus, and every delivery log run inside your own stack rather than a vendor’s.

Learn more about VBWD

VBWD is a self-hosted, source-available platform for building subscription products, marketplaces, and AI-powered apps. Explore it further:

VBWD is source-available — get the SDK on GitHub.

Happy
Happy
0%
Sad
Sad
0%
Excited
Excited
0%
Angry
Angry
0%
Surprise
Surprise
0%
Sleepy
Sleepy
0%

SaaS Is Dead: Long Live Self-Hosted

The Quiet Business Model Behind Half the Software You Use: OEM on VBWD

Leave a Reply

Your email address will not be published. Required fields are marked *

1 + 4 =