The simplest safe way to add payments to your app is to let a payment provider host the checkout page. Start with a Stripe Payment Link or Stripe Checkout, or with a merchant of record such as Paddle if you want someone else to handle sales tax. Then unlock paid features only when a verified webhook says the payment went through.
That way card numbers never touch your server. You also avoid the two classic mistakes in AI-written payment code: trusting a price sent by the browser, and trusting a webhook nobody verified.
- Payment Links need no code. Hosted Checkout needs one server route and a webhook. Custom payment forms are rarely worth it early on.
- As of September 2026, Stripe charges US businesses 2.9% plus 30 cents per domestic card payment. Merchants of record charge more but take on sales tax and VAT.
- The server decides the price. The browser only says which plan the customer picked.
- Unlock access from a verified webhook, never from the success page alone.
- Rehearse everything in test mode with Stripe’s test cards and CLI before real money moves.
Three ways to take payments
| Payment Link | Hosted Checkout | Custom payment form | |
|---|---|---|---|
| Code needed | None | One server route and a webhook | The most: your page plus Stripe’s embedded components |
| Where customers pay | A Stripe-hosted page | A Stripe-hosted page | Inside your own page |
| How access unlocks | By hand, or a webhook | A webhook | A webhook |
| Best for | Pre-orders and first sales | Most SaaS subscriptions | Custom flows once you have volume |
A Payment Link is the fastest test of whether anyone will pay. Create a product in the Stripe dashboard, copy the link and put it on your landing page. There is no extra fee: Payment Links and Checkout are included in standard processing.
Once payments must attach to user accounts, move to hosted Checkout. Your server creates a checkout session, Stripe hosts the payment page, and a webhook tells you when money arrived. Custom forms give you full control of the look, but they add code to write, test and maintain.
Do you need a merchant of record?
This matters because sales tax, VAT and GST on software often depend on where the buyer lives. Sell to customers in many countries and the paperwork can outgrow the product. Stripe Tax can calculate and collect tax for 0.5% per transaction on its no-code plan, but your business remains the seller.
- Tax registration, collection, filing and payment handled for you
- Fraud screening and chargebacks handled for you
- One provider answers customers’ billing questions
- Higher fees than Stripe alone
- Built for digital products; Stripe’s version excludes physical goods and services like consulting
- Less control over the checkout experience
A simple rule: if you sell mostly in your own country, Stripe alone is fine. If you sell software worldwide from day one and work alone, a merchant of record is usually worth its fee.
How much does it cost to take payments?
List prices as of September 2026, from each provider’s own pricing page. Stripe’s rates are for a business based in the US; other countries have their own:
| Provider | Who is the seller | Fee per payment | Worth knowing |
|---|---|---|---|
| Stripe | You | 2.9% + 30 cents (US cards) | +1.5% for international cards; subscriptions add 0.7% for Billing |
| Stripe Managed Payments | Stripe | Stripe’s fees + 3.5% | Digital products only, for businesses in supported countries, after an eligibility review |
| Paddle | Paddle | 5% + 50 cents | Tax, fraud and chargebacks included; custom pricing below $10 |
| Lemon Squeezy | Lemon Squeezy | 5% + 50 cents, plus small extra fees on some payments | Its January 2026 update says the goal is an easy move to Stripe Managed Payments |
| Polar | Polar | 5% + 50 cents on the free plan | +1.5% for international cards; lower rates on paid plans from $20 a month |
Percentages hide the real cost at small prices, so here is one $20 monthly subscription paid to a US business with a US card:
| Setup | Fee per payment | Sales tax |
|---|---|---|
| Stripe with Billing | $1.02 | Your job |
| Stripe with Billing and Stripe Tax | $1.12 | Calculated for you, still your filing |
| Paddle | $1.50 | Handled for you |
Disputes cost extra with Stripe: $15 for each one you receive, and another $15 if you respond to it by hand, which you get back if you win.
How money flows through your app
Create your prices in the dashboard
Set up products and prices in test mode first. Copy each price ID into an environment variable.
Create a checkout session on the server
When a signed-in user clicks Upgrade, your server creates a Checkout Session with the right price and the user’s ID, then sends them to Stripe’s page. Take that ID from the login session, as our guide to adding login explains.
Let the customer pay on Stripe’s page
Stripe collects the card, handles bank authentication and redirects the customer back to your success page.
Unlock access from the webhook
Stripe sends a
checkout.session.completedevent to your server. Verify it, check that itspayment_statusis notunpaid, then turn on the paid plan for that user, exactly once. Bank debits can finish checkout before the money arrives, and Stripe sendscheckout.session.async_payment_succeededwhen it does.Keep listening after the sale
Renewals, failed payments and cancellations arrive as events too, such as
invoice.payment_failedandcustomer.subscription.deleted. Update access when they do.
Stripe is blunt about step four. You cannot rely on the success page alone, because customers are not guaranteed to reach it. Someone can pay and lose their connection before it loads, and anyone can type its address.
Mistake one: letting the browser set the price
Ask an agent for a checkout button and it may send the price from the page to your server. Anyone can change that request in their browser’s developer tools. Edit it, and the vulnerable version asks Stripe to charge whatever amount the visitor typed.
- const { amount } = req.body; // the browser picks the price
- line_items: [{ price_data: { currency: "usd", unit_amount: amount, product_data: { name: "Pro" } }, quantity: 1 }],
+ const price = PRICES[req.body.plan]; // the server picks the price
+ line_items: [{ price, quantity: 1 }],Here, PRICES maps plan names to the price IDs from your dashboard, and an unknown plan name gets an error. The same rule covers quantities, discounts and trial lengths. The browser asks. The server decides.
Mistake two: trusting a webhook you did not verify
Your webhook address is public. Without a signature check, anyone who finds it can post a fake “payment succeeded” event and unlock your product for free. Stripe signs every event, and your server must check that signature before acting. This handler uses Express 5.2 and the stripe library 22.6:
import express from "express";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();
// Stripe signs the exact bytes it sends, so this route needs the raw body.
// Keep it above any app.use(express.json()) line.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.log(`Rejected: ${err.message}`);
return res.sendStatus(400);
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
// Bank debits can complete checkout before the money arrives.
if (session.payment_status !== "unpaid") {
// Unlock access here. Stripe may send the same event twice,
// so skip sessions you have already fulfilled.
console.log(`Paid: ${session.id}`);
}
}
res.sendStatus(200);
});
app.listen(4242, () => console.log("Listening on port 4242"));Sending it one properly signed test event and two fakes gives this:
Signed by Stripe: HTTP 200
Tampered amount: HTTP 400
No signature: HTTP 400The raw body comment matters. Put app.use(express.json()) above this route and even genuine events fail with “Webhook payload must be provided as a string or a Buffer.” That error is when an agent may “fix” things by deleting the check. Since August 2026 it could also reach for constructEventWithoutVerification, a helper meant for events you already verified elsewhere. In a public webhook route, either one is a red flag.
Two more rules from Stripe’s webhook guide. Return a 200 quickly, because Stripe retries failed deliveries for up to three days in live mode. And store the IDs you have processed, since the same event can arrive twice and events can arrive out of order. Our security checklist for AI-generated code covers the checks around this.
Test mode before real money
Stripe’s sandboxes let you run the whole flow without moving money. Use any future expiry date and any three-digit CVC with these test cards:
4242 4242 4242 4242succeeds.4000 0000 0000 0002is declined.4000 0025 0000 3155asks for extra authentication.
To send events to the server on your laptop, use the Stripe CLI. These commands need your own Stripe account, so run stripe login first:
# Forward test events to your local server
stripe listen --forward-to localhost:4242/webhook
# In a second terminal, fire a test payment event
stripe trigger checkout.session.completedstripe listen prints a signing secret that starts with whsec_. Use it as STRIPE_WEBHOOK_SECRET while you test locally.
FAQ
Should I use Stripe or Paddle?
Use Stripe if you want the lowest fees and the most control, and you sell mostly at home or can handle sales tax. Use Paddle or another merchant of record if you sell digital products worldwide and want tax, fraud and disputes off your desk.
Is it safe to put Stripe keys in my app?
Only the publishable key, which starts with pk_, belongs in the browser. The secret key and the webhook secret must stay on the server in environment variables. Our guide to keeping API keys safe covers the rest.
What happens when a customer disputes a payment?
The customer’s bank takes the money back while it reviews the case, and you can respond with evidence. As of September 2026, Stripe charges a US business $15 for each dispute, plus $15 to respond by hand, which comes back if you win. Merchants of record such as Paddle handle chargebacks as part of their fee.
Can an AI agent build my payment integration?
Yes, with test keys only. Then check the two mistakes above yourself: the server must set the price, and the webhook must verify the signature.
Next, see how payments fit into building a SaaS from idea to first paying user, or choose a database for your orders and users.
- Pricing and fees, Stripe, September 2026
- Managed Payments, Stripe Docs, September 2026
- Managed Payments eligibility, Stripe Docs, September 2026
- Pricing, Paddle, September 2026
- Pricing, Lemon Squeezy, September 2026
- 2026 update: Lemon Squeezy and Stripe Managed Payments, Lemon Squeezy, January 2026
- Fees, Polar, September 2026
- Receive Stripe events in your webhook endpoint, Stripe Docs, September 2026
- Fulfill orders, Stripe Docs, September 2026
- Testing, Stripe Docs, September 2026
- stripe-node v22.5.0 release notes, Stripe, August 2026




