@nomideusz/svelte-payments v0.9.0
Provider-agnostic payments for Svelte 5 booking products — hosted checkout, embedded card fields, subscriptions, refunds, and a webhook-safe settlement state machine. Currently ships a Mollie adapter (zero dependencies, plain fetch); the interfaces are deliberately shaped to fit Stripe Checkout next.
On this page
Installation#
npm install @nomideusz/svelte-payments
The model#
Amounts are always integer minor units (cents/grosze). The provider's webhook body is never trusted — the authenticated re-fetch of the payment is the verification.
import { mollie, createCheckout } from '@nomideusz/svelte-payments/server';
const provider = mollie(MOLLIE_API_KEY);
const checkout = createCheckout(provider, {
// your only integration point — all callbacks must be idempotent
savePayment: async (p) => db.upsertPayment(p),
onPaid: async (p) => confirmOrder(p.metadata?.orderId),
onFailed: async (p) => releaseHold(p.metadata?.orderId),
});
// 1. start — create the payment, redirect the customer to checkoutUrl
const payment = await checkout.start({
amount: 4900, currency: 'PLN',
description: 'Morning Vinyasa — booking YG-ABC12345',
redirectUrl: `${origin}/confirmed?payment=done`,
webhookUrl: `${origin}/api/webhooks/mollie`,
metadata: { orderId },
});
// 2. settle — from the webhook AND the customer's return trip (same function)
export const POST = ({ request }) => checkout.handleWebhook(request);
await checkout.settle(paymentId); // e.g. on the confirmation page
handleWebhook answers 400 for junk and forged ids (stop retrying) and rethrows real failures so your route 500s and the provider retries.
SvelteKit caveat: a plain
+server.tsPOST route is NOT enough on its own. Mollie postsapplication/x-www-form-urlencodedwith noOriginheader, and SvelteKit's built-in CSRF protection 403s exactly that shape — and it runs before any handle hook, so neither a hook norcsrf.trustedOriginscan exempt the route (trustedOriginsmatches Origin values; an Origin-less request never passes it). The pattern that works in production: disable the built-in check insvelte.config.js(csrf: { checkOrigin: false }, ortrustedOrigins: ['*']on newer Kit), then inhooks.server.tshandle the webhook path first and re-implement the origin check yourself for every other mutating request — otherwise every webhook delivery fails and Mollie keeps retrying.
Subscriptions#
Customers, mandates and recurring charges (Mollie: card mandates via a sequenceType: 'first' payment):
const customer = await provider.createCustomer({ email });
await provider.createPayment({ ...input, customerId: customer.id, sequenceType: 'first' });
// once paid, the mandate exists:
await provider.createSubscription({
customerId: customer.id, amount: 4900, currency: 'PLN',
interval: '1 month', description: 'Premium', startDate: nextMonth,
});
Refunds#
await provider.refund(paymentId, { amount: 4900, currency: 'PLN' });
Ledger (@nomideusz/svelte-payments/ledger)#
A pure financial-event-ledger subpath — no DB, no SDK, no env. It owns the event vocabulary, the idempotency-key grammar, and the one safe ordering for a keyed money movement; your app owns the table schema, the SDK calls, and the duplicate-recovery policy.
import { executeKeyedOperation, refundKey, derivePaymentStatus } from '@nomideusz/svelte-payments/ledger';
// refundKey encodes the CUMULATIVE amount, so a retry reuses the key while a
// genuine second refund gets a new one.
const res = await executeKeyedOperation(
store, // your LedgerStore: insert(event) atomic on eventKey, markCompleted(key)
{ eventKey: refundKey(bookingId, refundedSoFar + amount), type: 'refund', subjectId: bookingId, paymentId, amount, currency: 'PLN' },
(idempotencyKey) => provider.refund(paymentId, { amount, currency: 'PLN', idempotencyKey }),
);
if (res.status === 'executed') { /* update your totals */ }
derivePaymentStatus(totalMinor, refundedMinor); // 'paid' | 'partially_refunded' | 'refunded'
executeKeyedOperation appends the event row → calls the provider with the key
→ marks it completed. A duplicate key short-circuits without touching the
provider; a provider failure rethrows and leaves the row for your recovery
path. Amounts are validated as integer minor units. The vocabulary is
provider-neutral (charge / refund / chargeback); marketplace shapes
(transfers, payouts) are deliberately excluded from the core.
Embedded card fields#
MollieCard wraps Mollie Components (PCI SAQ-A hosted iframes) as a Svelte 5 component, themeable via --asini-* custom properties:
<script>
import { MollieCard } from '@nomideusz/svelte-payments';
let card;
</script>
<MollieCard bind:this={card} profileId="pfl_..." testmode locale="pl_PL" />
<button onclick={async () => submit(await card.createToken())}>Pay</button>
Pass the token as cardToken to createPayment — the customer skips the provider's method-selection page entirely (only 3-D Secure may still redirect).
License#
MIT