/ build-guides / Adding Real Payments To SvelteKit Without A Billing Monolith
build-guides 8 min read

Adding Real Payments To SvelteKit Without A Billing Monolith

How I added paid checkout to a SvelteKit app using Whop's embedded iframe, and why a verified server webhook, not the browser callback, grants access.

A SvelteKit checkout page with an embedded payment widget mounted inline

I needed a SvelteKit app to take real money, and the path of least resistance was the wrong one. The default move when you want payments is to reach for a full billing platform, the kind that wants to own your customer records, your subscription state, your pricing, and a big chunk of your frontend. For a side project run by one person, that is a heavy thing to adopt. It is a second source of truth bolted onto the side of an app that already has its own database and its own idea of who is allowed to do what.

I wanted a smaller surface. I wanted users to pay on my site, on a real page I control, and I wanted my own backend to remain the only authority on who has access. That constraint is what shaped the whole design, and it is the part worth teaching, because the same shape works for any non-React app that needs paid access without a billing monolith sitting in the middle.

The plan that came out of it has two pieces that look similar and are not. There is the checkout the user sees in the browser, and there is the webhook the server trusts. They both fire when a payment completes. Only one of them is allowed to grant anything. Getting that distinction right is the entire job.

Mounting Checkout Inline Instead Of Redirecting

The first decision was to not throw the user off my domain. A redirect to a hosted checkout page is the simplest integration on paper, and it is also the one that breaks the feeling that the user is still in my app. The moment the URL bar changes to someone else's domain, the experience stops being mine, and on a small product that continuity is part of the trust you are asking people for.

Whop supports an embedded iframe checkout, so I mounted it inline. The whole frontend integration is one small Svelte component. It pulls in Whop's checkout script, renders a container element, and points that container at the plan the user is buying. Whop's widget takes over inside the iframe and handles the card form, the validation, and the actual charge, all of which I emphatically do not want to build or touch myself. Card data never lands on my page or in my code. From the user's side it looks like a normal section of my own checkout page, because it is.

The component also carries a completion callback. Whop exposes this as an attribute you set on the embed, naming the page to send the user to when the charge finishes. So when the payment succeeds in the browser, the widget fires that callback and the user lands on my success page. That is a clean piece of UX. It is also the exact thing I had to be most careful about, because it is tempting to treat that moment as the moment access is granted. It is not. It is only a hint that something probably happened.

Why The Browser Callback Cannot Be Trusted

Here is the rule the whole system is built on. Anything that happens in the browser is a suggestion, not a fact. The completion callback runs on the user's machine, inside a page I shipped but no longer control once it loads. A determined person can call that callback by hand from the console without paying a cent. An honest person can have it never fire at all, because their tab crashed, their phone died, their network dropped at the wrong second, or they closed the window the instant the charge cleared. The callback is wrong in both directions, sometimes on purpose and sometimes by accident, and a payment system that grants access based on a browser event will eventually give away the product for free and also withhold it from someone who paid.

So the callback does exactly one thing. It improves the experience for the common case by moving the user to a nicer page. It never flips a database row. It never unlocks a feature. It is UX, full stop. If I deleted the callback entirely, no one who paid would lose access, because access does not come from there. That sentence is the test I hold the design against. If the answer were anything other than yes, the design would be wrong.

The Webhook Is The Source Of Truth

Access comes from the server, from a webhook, and only after the server has verified the message is genuine. When a charge completes, the payment provider sends a server-to-server request to an endpoint I expose. This request does not pass through the user's browser, so the user cannot forge it or interrupt it. That property is the entire reason the webhook exists and the entire reason it is trustworthy where the callback is not.

But a public endpoint that grants paid access the instant it receives a POST is its own disaster, because anyone on the internet can POST to it. So the first thing the webhook does, before it reads the payload as truth, is verify a signing secret. The provider signs each webhook with a shared secret, and my endpoint recomputes that signature over the raw request body and compares it. If the signature does not match, the request is rejected and nothing happens. Only a message that was actually signed by the provider with the secret only the two of us share gets to proceed. This is the line between a webhook and an open door.

Once the signature checks out, the work is small and deliberate. The webhook reads which plan was paid for and maps that plan to the right entitlement in my system, because the plan the user bought determines what they get. Then, and only then, it flips the order to paid in my own database. That database flip is the single event in the whole flow that means access has been granted. Everything before it is either UX or verification. The webhook is the one actor allowed to change what a user can do, and it earns that authority by proving the message is real before it acts on it.

There is a real ordering consequence to design around. The webhook and the browser callback race, and the webhook can land a moment after the user is already looking at the success page. So the success page cannot assume entitlement has been granted yet. It reflects state, it does not create it, and if the paid flag is not set the instant it loads, that is normal and the page resolves it by reading server state rather than asserting anything. The browser shows what is true. The server decides what is true.

Keep The Secrets Out Of The Code

The signing secret is the thing that makes the webhook trustworthy, which makes it the thing an attacker most wants. If it leaks, anyone can sign a fake webhook and grant themselves paid access, and the verification step becomes theater. The plan IDs matter for the same reason, because they are what the webhook maps to entitlements. So none of it lives in the source. The plan IDs and the webhook secret are stored as cluster secrets and injected into the running app as environment variables, never committed, never printed, never shipped to the browser. The repository describes how the integration works. It never contains the values that make it forgeable. That separation is not optional once a server endpoint is the gatekeeper for paid access.

I wired this same embedded-plus-webhook shape into a real pricing page on the SvelteKit directory I built solo, where the checkout mounts inline and the entitlement only lands after the verified webhook fires, and you can see the customer-facing side of it on the Tool Index pricing page.

What This Pattern Buys You

The lesson generalizes well past this one stack. You do not need to adopt a billing monolith to take real money, and you should be suspicious of any integration that wants to become a second source of truth in your app. Hold every payment design against one question. If I deleted the browser event, would anyone who paid lose access? If the answer is no, the server is the source of truth and the design is sound. If the answer is yes, something fakeable is guarding your product, and it is only a matter of time before that costs you.

I build things like this for clients, full-stack apps, AI agents, and automation pipelines, usually shipped faster than expected because I work with AI tooling every day. If you want something built, book a call.

Built by Kevin

Like this? You'll like what I'm building too.

Two ways to support and get more of this work.

Desktop App

HEARTH

A privacy-first Life OS for your desktop. Journal, tasks, and notes that stay on your machine. Coming soon, direct download from this site.

Read more
Digital Products

MY TOOLKITS

Receipts-first toolkits for shipping after hours, building Claude agents, publishing on Amazon, and more. The exact methods I used, not theory.

Browse on Whop

Need This Built?

Kevin builds products solo, from first version to live. If you want something like this made, work with him.