Stripe
You can accept payments in Val Town with Stripe. This guide teaches you the simplest method: Stripe Payment Links. Here’s how it works:
- When users click to pay on your site, you redirect them to your Stripe Payment Link.
- They pay with Stripe.
- Stripe redirects them back to your site, where you can now store their payment information in a database, and thank them for their payment.
Try it out

1. Create a Payment Link
Head over to Payment Links in the Stripe dashboard and click the “create payment link” button.
2. Configure your Payment Link
On the Payment page
tab, we’ll need to create a new product for our 1.00 tip.
On the After payment
tab, we need to select Don't show confirmation page
and fill out the input with our val’s /success
endpoint so we can redirect users there after a successful transaction.
3. Repeat for each tip amount
Repeat steps 1 - 2 for each tip amount that will be used in the app. Here’s how we handle all the payment links in the app:
const [tipAmount, setTipAmount] = useState(5); const paymentLinks = { 1: "https://buy.stripe.com/test_eVa5nA3FhblYg1y3cc", 5: "https://buy.stripe.com/test_6oE3fs7Vx0Hk4iQcMN", 10: "https://buy.stripe.com/test_aEU3fs4Jl89M7v26oq", 20: "https://buy.stripe.com/test_bIY4jwcbN61E2aI4gj", };
const handleTip = () => { window.location.href = paymentLinks[tipAmount]; };
4. Handling successful transactions
After configuring and creating your Payment Links, we need to give our val the ability to handle successful transactions. Here’s the code used in the example val above:
function Success() { useEffect(() => { confetti({ particleCount: 100, spread: 70, origin: { y: 0.6 }, }); }, []);
return ( <div className="container mx-auto max-w-md p-6 bg-white rounded-lg shadow-lg text-center"> <h1 className="text-2xl font-bold mb-4">Thank You for Your Tip!</h1> <p className="mb-4">Your generosity is greatly appreciated.</p> <a href="/" className="text-indigo-600 hover:underline">Back to Home</a> </div> );}
function client() { const root = document.getElementById("root"); if (root) { const url = new URL(window.location.href); if (url.pathname === "/success") { createRoot(root).render(<Success />); } ... } ...}
export default async function server(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname === "/success") { return new Response( ` <html> <head> <title>Tip Successful</title> <script src="https://cdn.tailwindcss.com"></script> </head> <body class="bg-gray-100 flex items-center justify-center min-h-screen"> <div id="root"></div> <script src="https://esm.town/v/std/catch"></script> <script type="module" src="${import.meta.url}"></script> </body> </html> `, { headers: { "content-type": "text/html", }, }, ); }
...
}
Take it further
Congratulations 🥳! You can now use Stripe in Val Town with Payment Links!

What if we wanted to allow users to tip custom amounts though? In order to give users the ability to tip custom amounts, we must use the more customizable Stripe Checkout.
Obtain credentials from the Stripe dashboard
Head over to the Stripe dashboard and get your secret key.
Add Stripe to your Val Town environment variables
Go to your Val Town environment varibales and add your secret key to Val Town.
Use Stripe in the server
...
if (url.pathname === "/create-checkout-session" && req.method === "POST") { const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY"), { apiVersion: "2022-11-15", });
const data = await req.json(); const { amount, name, comment } = data;
const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: [ { price_data: { currency: "usd", product_data: { name: "Tip", }, unit_amount: Math.round(amount * 100), }, quantity: 1, }, ], mode: "payment", success_url: `${url.origin}/success?session_id={CHECKOUT_SESSION_ID}&amount=${amount}&name=${ encodeURIComponent(name) }&comment=${encodeURIComponent(comment)}`, cancel_url: `${url.origin}`, });
return new Response(JSON.stringify({ url: session.url }), { headers: { "Content-Type": "application/json" }, }); }
...
...
if (url.pathname === "/success") { const sessionId = url.searchParams.get("session_id"); const amount = url.searchParams.get("amount"); const name = url.searchParams.get("name"); const comment = url.searchParams.get("comment");
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY"), { apiVersion: "2022-11-15", });
const session = await stripe.checkout.sessions.retrieve(sessionId); const paymentIntent = await stripe.paymentIntents.retrieve(session.payment_intent as string);
if (paymentIntent.status === "succeeded") { const sanitizedName = name.replace(/'/g, ""); const sanitizedComment = comment.replace(/'/g, ""); await sqlite.execute( `INSERT INTO ${KEY}_payments_${SCHEMA_VERSION} (name, amount, comment) VALUES (?, ?, ?)`, [sanitizedName, amount, sanitizedComment], ); }
const paymentsJson = JSON.stringify([{ name, amount, comment }]);
...}
...