One payments plugin for Vendure. Stripe, Adyen, PayPal, Mollie, Square, Braintree, GoCardless, Checkout.com, Coinbase Commerce, bank transfer and pay-later behind a single contract, so every provider gets the same features and every payment lands in the same ledger:
Hosted checkout page — one mutation and a redirect; the plugin's own page shows every enabled method, drives each provider's client and brings the customer back paid. Nothing provider-specific in your storefront.
Hosted / embedded sessions — Stripe Payment Element, Adyen Drop-in, PayPal buttons, Mollie hosted checkout; cards, Apple Pay, Google Pay, Link, iDEAL, Klarna, Bancontact, SEPA and whatever the provider enables. 3-D Secure / SCA handled by the provider.
Server-side verification — the storefront never decides a payment
state; the plugin re-reads the intent / session / order from the provider
and checks amount, currency and order code before addPaymentToOrder.
Capture control — automatic or manual capture per method, partial captures, cancels, full and partial refunds from the normal Vendure refund flow.
Signed, idempotent webhooks — Stripe signatures, Adyen HMAC (+ basic auth), PayPal signature verification, Mollie fetch-back. Duplicate deliveries are ignored; results update Vendure payments and refunds.
Disputes — chargebacks land in the ledger and alert ops.
Saved cards — Stripe Customers and Adyen stored payment methods for signed-in customers; list and remove from the shop API.
Subscriptions — mark any product variant as billed daily / weekly / monthly / yearly (with a trial); the order charges the first period, then Stripe, PayPal and Mollie bill natively and Adyen renewals are charged by the plugin's scheduler from the stored card. Customers see and cancel their subscriptions through the shop API; admins pause, resume, cancel and see MRR.
Pay by link — one click in the admin creates a provider-hosted payment link for any unpaid order (draft orders, accepted quotes, phone orders); the webhook settles it.
Routing, rules and surcharges — provider order per channel with fallback on decline, an eligibility checker (amount band, currencies, countries, customer groups, signed-in only) usable on any payment method, and optional per-provider surcharges.
Ledger + dashboard — authorisations, captures, refunds, disputes, failures, renewals and pay-links by provider and day, success rate, webhook deliveries, subscriptions, settings — under Sales → Payments.
Works on MySQL, MariaDB and PostgreSQL. No provider SDKs: each provider is
a few REST calls on the platform fetch.
Then open Sales → Payments → Providers and click Connect on a provider: paste the keys from its dashboard (linked from the card), click Test connection to see which account they belong to, then Connect. The plugin verifies the keys with the provider, registers the webhook itself (Stripe, PayPal, and Adyen when the credential has the Management API webhook role), stores the signing secret, and creates the Vendure payment method on the channel you chose — nothing to copy back by hand. Mollie needs no webhook setup at all.
Prefer the standard route? Settings → Payment methods → Create and pick
the handler HULO Payments — Stripe / Adyen / PayPal / Mollie; every field
explains where to find its value. Tables are created on boot; the only
migration is Vendure's own for the three subscription custom fields on
ProductVariant (npx vendure migrate on installs that use migrations).
Hosts on Vendure's UUID id strategy are supported: hulo_hosted_session.orderId
is text (installs from before 0.2.9 are widened on boot; if that ALTER is
refused the log names the statement to run by hand).
The simplest integration is the hosted page:
Opening the link is read-only: crawlers and chat link-previews fetching it
leave the basket editable. The order enters ArrangingPayment when the
customer picks a method (the page's first /session call).
Content-Security-Policy. The page carries a CSP that allows its own
script by a per-request nonce and every provider client by host (Stripe,
Adyen via jsDelivr, PayPal, Square, Braintree), with frame-ancestors 'none', base-uri 'none' and form-action 'self'. hostedCsp picks the
mode: 'report-only' (default) sends Content-Security-Policy-Report-Only
and logs what a browser would have blocked (browsers post reports to
POST /hulo-payments/csp-report, answered 204, logged at most 20 times a
minute with page tokens masked); 'enforce' blocks it; 'off' keeps only
the frame-ancestors / base-uri policy. Run report-only for a while, and once
the server log shows no hosted page CSP warnings for the providers you
offer, switch to 'enforce'. The Stripe flow is verified under 'enforce'
on every release; a provider that loads a host not on the list would show
up in report-only mode first.
For an embedded checkout, drive the providers yourself:
| Provider | Client library | metadata for addPaymentToOrder |
|---|---|---|
| Stripe | @stripe/stripe-js Payment Element with clientSecret | { paymentIntentId } |
| Adyen | @adyen/adyen-web Drop-in with sessionId + sessionData, clientKey, environment | { sessionId, sessionResult } from onPaymentCompleted |
| PayPal | @paypal/paypal-js buttons with createOrder: () => sessionId | { paypalOrderId } |
| Mollie | redirect to checkoutUrl; on return | { molliePaymentId } (the sessionId) |
| Square | Web Payments SDK with config.applicationId / locationId | { sourceId } (the card token) |
| Braintree | Drop-in with clientSecret as the authorization | { nonce, deviceData } |
| GoCardless | redirect to checkoutUrl; on return | { billingRequestId } (the sessionId) |
| Checkout.com | redirect to checkoutUrl; on return | { sessionId } |
| Coinbase Commerce | redirect to checkoutUrl; on return | { chargeCode } (the sessionId) |
| Bank transfer / pay later | show instructions | {} |
Also available: huloSavedPaymentMethods, huloRemoveSavedPaymentMethod,
huloMySubscriptions, huloCancelSubscription(id, atPeriodEnd),
huloApplyPaymentSurcharge(methodCode).
Set Subscription billing interval (and optionally every N intervals
and free trial days) on a product variant. Its price is the price per
period. When an order containing it is paid through a HULO method the
plugin creates the subscription with the provider (or schedules it itself
for Adyen), records renewals in the ledger, alerts on failed renewals and
cancels after the configured number of failed daily attempts. PayPal
subscriptions need one customer approval; the approval link is exposed on
huloMySubscriptions and in the admin. Renewal periods are calendar months
and years (a monthly plan bills on the same day each month). The renewal
scheduler runs hourly on the worker; the plugin registers Nest's
ScheduleModule itself, so no host wiring is needed. Set
disableScheduler: true to run it elsewhere.
Set-up checklist. The top of the Payments page lists what is still to
do, each item checked against the real state of the install: a provider
connected, a method enabled on a channel, hostedReturnHosts set, an ops
email, every connected provider's webhook secret saved, and a first payment
in the ledger. "Hide" puts it away until something that was done comes
undone; it disappears for good once everything is done.
Weekly report. Every Monday at 08:00 (server-local time) the plugin emails what it did last week — payments settled and their value, refunds, disputes opened, active subscriptions and MRR, pay-by-link takings and failed renewals it went on to recover — each against the previous week, plus up to three next steps from the checklist and, on the free tier or during the evaluation, what a licence adds. Every figure comes from the ledger; nothing is estimated.
ops.email option, then any channel's
Settings → Operations address), then the licence holder, then the
address left in "Email me before it ends".SMTP_SERVER (or
SMTP_HOST), SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM in
the host environment, with nodemailer installed on the host. Without
SMTP nothing is sent and nothing fails.disableScheduler: true.Dashboard widget. Dashboard → Add widget → Payments shows takings,
settled payments and disputes for the last 7 days, the licence notice when
a trial or key is about to end, and a link to the Payments page. Needs
ReadOrder.
Ops alerts. Disputes and failed renewals go to the channel's own
Settings → Operations address first, then the ops.email plugin option.
The anonymous usage numbers (counts and totals, never customer data) that
the evaluation ping sends to the licence server keep flowing after
purchase via the daily heartbeat; HULO_HEARTBEAT_DISABLED=true stops
them.
| Free tier | Licensed |
|---|---|
| Stripe: sessions, wallets, 3-D Secure, automatic/manual capture, refunds, disputes, signed webhooks; bank transfer; pay later | Adyen, PayPal, Mollie, Square, Braintree, GoCardless, Checkout.com, Coinbase Commerce |
| Hosted checkout page | |
| Ledger, dashboard, webhook log | Subscriptions and the renewal scheduler |
hulo-payment-rules eligibility checker | Saved cards, pay-by-link, provider routing, surcharges |
Unlicensed installs run everything for 14 days; the trial (card required, nothing charged until day 15) and the licence are bought from the admin banner. Details and pricing: https://huloglobal.com/vendure-plugins/payments/
Stripe payment_intent.succeeded / amount_capturable_updated / payment_failed / canceled, charge.refunded, charge.refund.updated, refund.updated, charge.dispute.created / closed, checkout.session.completed, invoice.paid / payment_failed, customer.subscription.updated / deleted / paused.
Adyen AUTHORISATION, CAPTURE, CAPTURE_FAILED, CANCELLATION, REFUND, REFUND_FAILED, CHARGEBACK, NOTIFICATION_OF_CHARGEBACK, CHARGEBACK_REVERSED, RECURRING_CONTRACT.
PayPal CHECKOUT.ORDER.APPROVED, PAYMENT.CAPTURE.*, PAYMENT.AUTHORIZATION.*, CUSTOMER.DISPUTE.*, BILLING.SUBSCRIPTION.*, PAYMENT.SALE.COMPLETED.
Mollie: every payment webhook (status, refunds, chargebacks, subscription payments).
All notable changes to @huloglobal/vendure-plugin-payments are
documented here. The format follows
Keep a Changelog and this project
adheres to semantic versioning.
GET/POST /hulo-payments/digest/settings, POST …/digest/send-test,
GET …/digest/preview. Uses the ops SMTP transport; nothing is sent
without SMTP_*. Worker-only, once per install across replicas, off
with disableScheduler.GET /hulo-payments/setup): provider connected, method enabled on a
channel, hostedReturnHosts set, ops email, webhook secret for every
connected provider, first payment taken. Replaces the "Three steps"
card. "Hide" is remembered in the browser and the card returns if a
done item comes undone.GET /hulo-payments/widget).paymentsSettled,
volumePence, refundedPence, disputesOpened, subscriptionsActive,
mrrPence, payLinkPaidPence, renewalsRecovered.GET /dashboard as
renewalsRecovered.eval/remind-me
route, which now also remembers the address as the weekly report's
last-resort recipient). GET /meta gains notice, premiumFeatures
and recentStats.@huloglobal/vendure-licence-sdk ^0.15.1; the heartbeat reports the jti of the key currently in force (env key, admin activation or buy-from-admin claim), not only the boot-time one.buyUrl: https://elite.charity/licence/buy/vendure-plugin-payments;
the admin UI turns any 402 (or licence-required body) into "SMTP_HOST as an alias of SMTP_SERVER and works
without SMTP_USER (unauthenticated relays).ops.email
plugin option; the channel setting was saved but never used.&, ', …) that Vendure's rich-text editor stores, so "Card & Apple Pay" no longer shows as Card & Apple Pay.Follow-up to the 0.2.8 audit — no new features. One column type change on
hulo_hosted_session is applied on boot; nothing else to migrate.
script-src,
frame-src and connect-src, with img-src * data:, style-src 'self' 'unsafe-inline', frame-ancestors 'none', base-uri 'none',
form-action 'self'. New plugin option hostedCsp: 'enforce' | 'report-only' | 'off' (default 'report-only': the policy is sent as
Content-Security-Policy-Report-Only and violations posted to the new
POST /hulo-payments/csp-report route are logged, rate-limited, tokens
masked). The Stripe hosted flow was verified under 'enforce' in a real
browser (Payment Element mounted, session created, zero violations).src/pg-corpus.test.ts, runs when
HULO_PG_URL is set): every SQL statement in src/** is translated by
the licence-sdk adapter and prepared against a scratch PostgreSQL 17 with
quoted-camelCase stand-ins for the Vendure tables the plugin reads.
66 statements pass.dist (they were published
with the package before).confirmPayment insisted
on a Drop-in sessionId + sessionResult, which a payment link does not
have. The AUTHORISATION / CAPTURE notification now confirms the payment
(Authorized or Settled per the event, transactionId = pspReference,
amount and currency checked against the order). The facts are registered
by the webhook path and consumed once by the handler, so a storefront
cannot forge a pspReference through addPaymentToOrder.sessionResult is now kept in the payment metadata.markSettledByWebhook) is a map with a 60 s
TTL and is cleared in a finally around settlePayment, so a failed
settle can no longer leave a stale "trust me" entry for that payment.huloRenewal and their payment_intent.* events ignored; Adyen
notifications whose merchant reference is a scheduler reference
(<order>-R<yyyymmdd>-<variant>-a<n>) are ignored. The scheduler's
renewal row is the only one.refund.settled now
targets the Vendure refund by the provider's refund id (Stripe re_… from
charge.refunded's refund list or the new refund.updated /
charge.refund.updated handling, Adyen's REFUND pspReference), then by
exact amount, then the only pending one — never "the first pending refund"
when several are open. A refund already settled from the handler is not
written to the ledger a second time.amount − Σ lines (Klarna / Afterpay reject sessions whose lines do not
sum to the amount).ArrangingPayment, so link previews and crawlers locked baskets. The GET
is read-only (an AddingItems order still lists its providers); the
transition happens in POST …/session, before the provider session is
created.cancelSubscription uses the stored customer reference instead
of listing up to 250 subscriptions to find it; the provider contract
gained an optional providerCustomerRef argument.hulo_hosted_session.orderId was INT; it is now
VARCHAR(36) (widened on boot with a guarded ALTER, MariaDB/MySQL and
Postgres) and compared as text.Reliability and security pass — no new features. Two columns on
hulo_hosted_session, a unique key on hulo_subscription, one index on
hulo_payment_txn and a best-effort index on Vendure's payment table are
added on boot; nothing to migrate.
payment.settled arriving before the customer
returns) called addPaymentToOrder outside a transaction, which Vendure
3.7 rejects; the event was then marked processed and lost. Every webhook is
now applied inside one transaction with the order row locked, in the
order's own channel with that channel's method code.checkout.session.completed + payment_intent.succeeded pair)
can no longer record two payments: both paths lock the order and skip a
reference that is already on a live payment. The hosted /complete route
also holds a one-minute lease on the session row so a double submit is
answered from the order's state.UPDATE before the provider is called (the hourly cron and
the admin "run now" run in different processes); the charge reference
includes the attempt number so a retry never replays a cached refusal.
Subscription rows are claimed with a pending row + unique key before the
provider subscription is created (the bus fires for PaymentAuthorized and
PaymentSettled seconds apart). Monthly and yearly periods are calendar
arithmetic instead of 30/365 days. The plugin registers ScheduleModule
itself, so renewals run on hosts that never wired it; disableScheduler
is honoured.returnUrl and
cancelUrl must be absolute http(s) URLs (optionally restricted with the
new hostedReturnHosts option); methodCode and locale are validated;
client metadata is allow-listed and server-side session ids win; provider
and Vendure error text no longer reaches the customer; clickjacking and
referrer headers are set; each link allows at most 30 provider sessions;
expiry is stored as a date (no UTC/local drift); expired rows are purged.channelId, paymentMethodId, transactionId, orderId); the
subscription updater no longer quotes the plugin's own columns; the
channel lookup goes through the dialect adapter. Payment methods were
silently absent on Postgres before.order.totalWithTax and the webhook accepts it; GoCardless
refunds send the required total_amount_confirmation; refund
idempotency keys are per minute, not per second.ALTER TABLE … IF NOT EXISTS is replaced by an
information-schema check (MySQL 8 boots again).trackBy on every table, daily bars
computed once per load, tab switches keep loaded data (Refresh reloads),
every provider named in Transactions/Subscriptions/Routing and available
in the Surcharges table, the providers cache expires after 60 s and is
cleared after Connect, the pay-by-link capability chip reads the right
key, the method panel shows the method being edited, and the action-bar
button needs only ReadOrder.hulo_payment_event rows older than 180 days and expired hosted sessions
older than 7 days are purged.vendureOrderId stamp) are acknowledged and ignored instead of being applied to the order; the plugin never adds a payment to an order that already carries another method's payment.POST /hulo-payments/methods { provider, channelId } behind it (admin, CreatePaymentMethod), idempotent per provider and channel.huloHostedCheckout(methodCode): the storefront can preselect one of the enabled methods, so a "Pay with PayPal" button opens the hosted page straight on PayPal. Without it the page opens on the first method in the channel's order, as before.?tab= and ?connect=<provider> deep links.huloHostedCheckout(returnUrl) returns a URL the storefront redirects to; the page shows every enabled method in the channel's preferred order, drives each provider's own client (Stripe Payment Element, Adyen Drop-in, PayPal buttons, Square Web Payments, Braintree Drop-in, hosted redirects, bank-transfer instructions), records the payment and sends the customer back with ?order=&result=. No provider code in the storefront. Branding (name, colour, logo) per channel in Settings./hulo-payments/webhook/<provider>, updating payments, refunds and subscriptions; disputes recorded and alerted.hulo-payment-rules eligibility checker, optional surcharges.