Adding a payment provider to most platforms is a project. You touch the checkout flow, the webhook handler, the refund logic, the reconciliation code, and a dozen places that assumed the old provider's shape. VBWD is built so that a payment provider is a plugin behind a single adapter interface — the rest of the system only ever talks to the adapter, never to a specific processor. This post walks through what that adapter is, what implementing one actually involves, and why "one file" is close to literally true.

The problem with hard-wired payments

When a codebase talks to Stripe directly, Stripe's assumptions leak everywhere. The shape of a charge, the way webhooks arrive, the id format, the refund semantics — all of it gets baked into code that has nothing to do with payments. Then a customer in a region Stripe does not serve well asks for a local provider, and you discover the cost: the provider is not a component you swap, it is an assumption woven through the whole app. Every place that touched "the payment" has to be found and generalised.

The fix is an old one — program to an interface, not an implementation — applied specifically to money movement. VBWD defines what a payment provider must be able to do, expresses that as an adapter (a port), and makes every provider a plugin that implements it. The checkout, the invoice lifecycle, and the reconciliation logic depend only on the port. Swapping or adding a provider does not ripple outward, because nothing outside the plugin knows which provider is in use.

The adapter: a small, explicit contract

A payment provider in VBWD is an SDK adapter with a narrow, predictable surface. Stripped to essentials, a provider needs to be able to do a handful of things:

That is the shape of the contract. The exact method names live in the codebase, but the point is the size: it is the irreducible set of operations any processor supports, expressed once. Your plugin implements those methods against your provider's SDK or REST API. Everything else — how the amount was calculated, which invoice it belongs to, what happens after payment — is not your plugin's concern, because the billing engine already owns it.

What "one file" means in practice

The claim is not that a production-grade integration is a single line — it is that the code you write is confined to the plugin implementing the adapter, and for a straightforward provider that is essentially one implementation module. You are not editing checkout, not editing the invoice model, not editing reconciliation. You write a class that satisfies the port by calling your provider's API, register it, and enable the plugin. The three providers that ship — Stripe, PayPal, and YooKassa — are each exactly this: an adapter implementation, not a fork of the platform.

The translation layer is where the real work sits, and it is bounded work: map the platform's "authorize this amount in this currency for this reference" onto your provider's create-payment call, and map your provider's webhook payload back onto the platform's captured/failed events. Once those two translations are correct, the provider is fully wired, because the rest of the system was already speaking the adapter's language.

Webhooks: translate, don't leak

The single most important discipline when writing a payment plugin is at the webhook boundary. Providers announce what happened — a payment succeeded, a charge was disputed — via webhooks in their own format. Your adapter's job is to verify the webhook's authenticity (signature check against your provider's secret) and then translate the provider-specific event into the platform's domain-neutral event. Downstream, the rest of VBWD reacts to "payment captured," not to "Stripe charge.succeeded." That translation is what keeps the provider swappable: two different providers emit the same internal event, and everything that depends on payment completion works identically for both.

Two things to get right here. First, verify the signature before trusting anything — an unauthenticated webhook endpoint is a way to fake payments. Second, and this is a hard-won platform lesson: if your plugin writes to the database inside an event callback, it must commit its own session. The event bus hands you the event; managing the transaction around your write is your responsibility, and a forgotten commit is a payment that "succeeded" but left no trace.

Registering the plugin

Making the provider available follows the standard plugin path. The plugin declares its metadata, registers its adapter implementation with the platform's dependency-injection container in its enable hook, and is turned on through the single plugin-management source of truth. Because registration goes through the DI container, the checkout resolves the active provider by interface — it asks for "the payment provider" and gets whichever one is enabled. You do not wire your provider into a central switch statement; you add it to the container and enable it.

One deployment reality worth stating: a plugin's DI registration must actually run on the full runtime path, not just in a test. Validate that enabling the plugin in a real instance results in the checkout resolving your provider — a provider that registers in a unit test but not in the running app is a classic and confusing failure.

Why this is a business capability, not just clean code

The architectural tidiness has a direct commercial payoff: payment coverage becomes a configuration decision per deployment rather than an engineering project. A customer selling into Europe wants SEPA and iDEAL; one in Southeast Asia wants local wallets; an OEM redistributing your product wants to bring their own processor. Because providers are swappable plugins, each deployment enables the rails it needs without a code change to the platform. The roadmap of additional providers — the payment ecosystem beyond the three that ship — is not a series of rewrites; it is a series of adapters, each isolated in its own plugin, each following the same contract.

For an OEM or agency, this is the difference between "we support the processors we hard-coded" and "we support the processor you need, enabled per client." The adapter pattern turns payment flexibility into a selling point instead of a backlog.

The honest limits

The adapter bounds the work; it does not eliminate the provider's inherent complexity. Some processors have genuinely awkward flows — multi-step confirmations, asynchronous settlement, region-specific compliance steps — and your adapter has to model those honestly within the contract. Testing against a provider's sandbox is non-negotiable before you take real money, because the failure modes (a webhook that arrives twice, a payment that settles hours later, a partial capture) only show up against the real API. And you own PCI-scope decisions and the provider's compliance requirements; the adapter isolates the code, not your legal obligations. What the pattern guarantees is that all of this complexity stays inside the plugin, where it belongs, instead of leaking into the rest of your product.

The takeaway

A payment provider in VBWD is a plugin that implements one small adapter: authorize, capture, handle-webhook, refund. The checkout and billing engine depend on the adapter, never on a specific processor, so adding a provider means writing one implementation that translates between the platform's domain-neutral payment events and your provider's API — then registering it in the DI container and enabling the plugin. The three shipped providers are exactly this shape, and every future rail is another adapter, isolated in its own plugin. Payment coverage stops being a rewrite and becomes a setting.

Copy an existing payment plugin as a template, implement the adapter against your provider's sandbox, register it in your enable hook, and verify the checkout resolves it on the real runtime before going live.