Guides // Selling software // Self-hosted checkout

Self-hosted digital delivery, no platform cut

Putting a software product on a hosted marketplace is the fastest way to take a first payment, but it also puts someone else's percentage, someone else's outage window and someone else's rules between you and your buyer. The alternative is running the checkout yourself: your own Stripe account, your own webhook, your own record of who paid for what. It is a handful of moving parts, and getting the order wrong is the whole risk, so this guide walks through what has to happen, in what order, before a single line of code.

The jobs

What a self-hosted checkout has to do

Selling a digital product yourself comes down to five jobs, and skipping any one of them either loses a sale or gives something away for free. Take the payment through a processor you do not have to build yourself. Confirm the payment actually happened before anything is delivered. Turn that confirmation into one durable record, a license key or an order row, that outlives the browser tab it was created in. Gate the download or the setup instructions behind that record, not behind a URL parameter. And leave yourself a way to revoke or refund a sale without editing a database by hand at midnight.

  • Payment through a processor, never a card number you handle yourself.
  • Payment confirmed server-side before delivery, not assumed from a redirect.
  • One durable entitlement record per sale, not a value trusted from the client.
  • A download gated on that record, with the private file path never reaching the browser.
  • A revoke or refund path that does not need direct database access to use.

Under the hood

Why a webhook, not a redirect

The tempting shortcut is to read the checkout's success URL and hand over the product right there, because the buyer is standing on that page and the query string looks like proof. It is not. That page can be reloaded, bookmarked, revisited after a card decline, or shared, and none of that involves the processor confirming anything to your server. The only trustworthy signal is a webhook event the processor sends directly to your backend, and even that is only trustworthy once you verify its signature against a secret only you and the processor hold. An unsigned or wrongly signed event has to be rejected before it touches anything, and a stale one past a short replay window should be rejected too.

Once a webhook event is verified, it still is not simple. A processor retries a webhook call until your endpoint answers with success, which means the same paid event can arrive more than once for one sale. The handler has to be idempotent: the second delivery of an event it already processed should find the existing entitlement and answer the same way, not mint a second key. And whatever the handler decides belongs in your own database as the entitlement of record. A price, a product name and a session id passed back from the client are inputs to double-check against your own catalog and your own webhook state, never facts to trust on their own.

Common gaps

Where a first attempt usually breaks

Four gaps show up often enough to name directly. Trusting the success redirect as proof of payment, covered above, is the most common one. Second is skipping idempotency, so a slow network or a processor retry issues two keys for one sale and the second buyer support ticket is the first anyone notices. Third is gating a download purely on a session id existing, rather than checking that the session belongs to that exact product and was actually paid, which lets a cancelled or wrong-product session through. Fourth is having no plan for the payment succeeding while the confirmation step, an email or a receipt document, fails: that order needs to end up somewhere pending and retryable, not silently lost with the buyer's money already taken.

The tool

A storefront that already does this

AL Store Platform is this exact pattern, already built: Stripe Checkout with the price read from a server-side catalog rather than the request, a signed webhook that issues one entitlement per paid session and stays idempotent on retries, and a gated download that only streams once that entitlement is confirmed. It runs on Cloudflare Pages Functions with a D1 database, so there is no server of your own to keep patched, and it includes a free lead-magnet path for a no-payment product alongside the paid ones. It pairs with AL License System for the key format and the validate/revoke API underneath, so the entitlement a sale creates is something a product can actually check on boot, not just a row nobody reads again.

Questions

Do I need to run my own server for this?

No. A checkout, webhook and gated download built on Cloudflare Pages Functions and D1 runs without a server to patch or restart, and scales to zero when nobody is buying.

What actually proves a customer paid?

A signed event from your payment processor, verified with your webhook secret, not the URL the customer's browser happens to land on. The redirect page is a convenience for the buyer, not evidence for your database.

What happens if the webhook fires twice for one sale?

A payment processor retries a webhook until it gets a success response, so the handler has to treat the same event arriving twice as normal and issue exactly one key, not two.

Is this only for FiveM scripts?

No. The pattern is generic: any downloadable software, a license key, or a Discord-linked entitlement can sit behind the same checkout, webhook and gated download.