How I Built My Own Embedded Stripe Checkout on a Squarespace Page
I wanted a checkout form that lived directly on my landing page — no redirect, no third-party cart platform, no clunky handoff. Here’s how I did it with Squarespace, Stripe, and a tiny Cloudflare Worker.
I’ve used Squarespace for years.
I love it for what it is good at: pages, design, content, and getting something online without turning every idea into a full software project.
But there has always been one thing that bothered me.
Checkout.
Not the basic “sell a product” version of checkout.
I mean the kind of checkout where someone is on a landing page, already convinced, and the payment form is right there.
No redirect.
No clunky third-party checkout page.
No “click here and leave my site.”
That kind of checkout is usually why people pay for tools like SamCart, ThriveCart, SendOwl, Lemon Squeezy, or other cart platforms.
But I wanted to see if I could create my own version using:
- Squarespace for the page
- Stripe for payment
- Cloudflare Workers for the secure backend logic
And it worked.
This tutorial walks through the basic setup.
What we’re building
We’re going to add a Stripe embedded checkout form directly inside a Squarespace page.
The basic flow looks like this:
Visitor lands on Squarespace page
↓
Squarespace loads Stripe.js
↓
The page asks a Cloudflare Worker to create a Checkout Session
↓
The Worker securely talks to Stripe
↓
Stripe returns a client secret
↓
The checkout form appears on the Squarespace page
↓
Customer pays
↓
Stripe sends them to a thank-you/download page
Squarespace handles the page.
Cloudflare Worker handles the secure server-side request.
Stripe handles the checkout.
That’s the whole architecture.
Why you need the Cloudflare Worker
The most important thing to understand is this:
Your Stripe publishable key can go on your website.
Your Stripe secret key should never go on your website.
The secret key is what allows your code to create checkout sessions, interact with payments, and make real requests to your Stripe account. If you put that key inside a Squarespace code block, anyone could inspect the page and find it.
That is why we use a Cloudflare Worker.
The Worker acts like a tiny backend.
Your Squarespace page asks the Worker:
Can you create a checkout session?
Then the Worker talks to Stripe using your secret key.
Stripe sends back a client_secret.
Your Squarespace page uses that client_secret to mount the embedded checkout form.
What you need before starting
You’ll need:
- A Stripe account
- A Stripe product and price
- Your Stripe publishable key
- Your Stripe secret key
- A Cloudflare account
- A Cloudflare Worker
- A Squarespace page where you can add a Code Block
In Stripe, create your product first.
For example:
Product: Find Your Hidden Asset
Price: $7
Type: One-time payment
Then copy the Price ID.
It should look like this:
price_123456789
Do not use the product ID. You need the price ID.
Step 1: Create the Cloudflare Worker
Create a new Worker in Cloudflare.
This Worker will create the Stripe Checkout Session.
Here is the Worker code:
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: corsHeaders(request),
});
}
const validPaths = [
"/create-checkout-session",
"/create-client-session"
];
if (validPaths.includes(url.pathname) && request.method === "POST") {
try {
if (!env.STRIPE_SECRET_KEY) {
return jsonResponse(
{ error: "Missing STRIPE_SECRET_KEY" },
500,
request
);
}
if (!env.STRIPE_GUIDE_PRICE_ID) {
return jsonResponse(
{ error: "Missing STRIPE_GUIDE_PRICE_ID" },
500,
request
);
}
const body = new URLSearchParams();
body.append("mode", "payment");
body.append("ui_mode", "embedded");
// Shows the discount / promo code field in Stripe Checkout.
body.append("allow_promotion_codes", "true");
// Send buyers here after successful purchase.
body.append(
"return_url",
"https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}"
);
// Main product.
body.append("line_items[0][price]", env.STRIPE_GUIDE_PRICE_ID);
body.append("line_items[0][quantity]", "1");
// Optional add-on product.
// Only included if STRIPE_WORKSHOP_PRICE_ID is set in Cloudflare.
if (env.STRIPE_WORKSHOP_PRICE_ID) {
body.append("optional_items[0][price]", env.STRIPE_WORKSHOP_PRICE_ID);
body.append("optional_items[0][quantity]", "1");
}
const stripeResponse = await fetch(
"https://api.stripe.com/v1/checkout/sessions",
{
method: "POST",
headers: {
"Authorization": `Bearer ${env.STRIPE_SECRET_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body,
}
);
const session = await stripeResponse.json();
if (!stripeResponse.ok) {
return jsonResponse(
{
error: "Stripe API error",
status: stripeResponse.status,
details: session,
},
stripeResponse.status,
request
);
}
if (!session.client_secret) {
return jsonResponse(
{
error: "Stripe did not return a client_secret",
session,
},
500,
request
);
}
return jsonResponse(
{
clientSecret: session.client_secret,
},
200,
request
);
} catch (error) {
return jsonResponse(
{
error: "Worker error",
message: error.message,
},
500,
request
);
}
}
return new Response("Not found", {
status: 404,
headers: corsHeaders(request),
});
},
};
function corsHeaders(request) {
const origin = request.headers.get("Origin");
const allowedOrigins = [
"https://yourdomain.com",
"https://www.yourdomain.com"
];
return {
"Access-Control-Allow-Origin": allowedOrigins.includes(origin)
? origin
: "https://yourdomain.com",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
}
function jsonResponse(data, status = 200, request) {
return new Response(JSON.stringify(data), {
status,
headers: {
...corsHeaders(request),
"Content-Type": "application/json",
},
});
}
A few important notes:
- Replace
https://yourdomain.com/thank-youwith your real thank-you page. - Replace the allowed origins with your actual domain.
- Keep your Stripe secret key out of the code itself.
- Add it as a Worker secret/environment variable instead.
Step 2: Add your Worker variables
In Cloudflare, add these variables/secrets:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_GUIDE_PRICE_ID=price_...
STRIPE_WORKSHOP_PRICE_ID=price_...
The workshop price ID is optional.
For my setup, the first product was a $7 guide.
The optional product was a $17 workshop.
If you don’t want an add-on, leave out STRIPE_WORKSHOP_PRICE_ID.
Step 3: Add the checkout to Squarespace
On your Squarespace page, add a Code Block.
Paste this into the Code Block:
<div class="checkout-wrapper">
<div class="checkout-intro">
<p class="checkout-kicker">Secure checkout</p>
<h2>Get Find Your Hidden Asset</h2>
<p>$7 · Google Doc + PDF · Instant access after purchase</p>
</div>
<div id="checkout" class="checkout-box">
<p>Loading checkout…</p>
</div>
</div>
<script src="https://js.stripe.com/v3/"></script>
<script>
const checkoutEl = document.querySelector("#checkout");
const stripe = Stripe("pk_live_YOUR_PUBLISHABLE_KEY");
async function initialize() {
try {
checkoutEl.innerHTML = "<p>Creating checkout session…</p>";
const response = await fetch("https://YOUR_WORKER_URL/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json"
}
});
const data = await response.json();
if (!response.ok || !data.clientSecret) {
checkoutEl.innerHTML = `
<p><strong>Checkout could not load.</strong></p>
<pre style="white-space: pre-wrap;">${JSON.stringify(data, null, 2)}</pre>
`;
console.error("Checkout session error:", data);
return;
}
checkoutEl.innerHTML = "";
const checkout = await stripe.initEmbeddedCheckout({
clientSecret: data.clientSecret
});
checkout.mount("#checkout");
} catch (error) {
checkoutEl.innerHTML = `
<p><strong>Checkout could not load.</strong></p>
<p>${error.message}</p>
`;
console.error("Checkout error:", error);
}
}
initialize();
</script>
<style>
.checkout-wrapper {
max-width: 900px;
margin: 56px auto;
padding: clamp(24px, 4vw, 44px);
background: #f7f1e7;
border: 1px solid rgba(23, 59, 47, 0.16);
border-radius: 22px;
}
.checkout-intro {
text-align: center;
margin-bottom: 28px;
}
.checkout-kicker {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: 700;
opacity: 0.72;
margin-bottom: 12px;
}
.checkout-intro h2 {
font-size: clamp(34px, 5vw, 58px);
line-height: 0.95;
letter-spacing: -0.05em;
margin: 0 0 10px;
}
.checkout-intro p {
margin: 0;
}
.checkout-box {
max-width: 640px;
margin: 0 auto;
background: #fff;
border-radius: 18px;
padding: 18px;
box-shadow: 0 18px 50px rgba(23, 59, 47, 0.08);
}
</style>
Replace:
pk_live_YOUR_PUBLISHABLE_KEY
with your Stripe publishable key.
Replace:
https://YOUR_WORKER_URL/create-checkout-session
with your Worker endpoint.
For example:
https://checkout.your-worker.workers.dev/create-checkout-session
Step 4: Make sure the paths match
This is the thing that got me at first.
My Squarespace page was calling one endpoint, but my Worker was listening for another.
The frontend was calling:
/create-client-session
But the Worker was listening for:
/create-checkout-session
So the checkout stayed stuck on:
Loading checkout…
Once I made the paths match, it worked.
In the Worker code above, I allowed both paths:
const validPaths = [
"/create-checkout-session",
"/create-client-session"
];
That gave me a little more room for error while testing.
Step 5: Enable discount codes
The Worker code includes this line:
body.append("allow_promotion_codes", "true");
That tells Stripe Checkout to show a promotion code field.
After that, create the actual coupon and promotion code in Stripe.
For example:
Code: EARLY
Discount: 50% off
Now the customer can enter that code during checkout.
Step 6: Add an optional workshop
I wanted the option to sell a $17 workshop alongside the $7 guide.
That is where this part comes in:
if (env.STRIPE_WORKSHOP_PRICE_ID) {
body.append("optional_items[0][price]", env.STRIPE_WORKSHOP_PRICE_ID);
body.append("optional_items[0][quantity]", "1");
}
That uses a second Stripe price ID.
The main item is the $7 guide.
The optional item is the $17 workshop.
This is the closest version of the “order bump” style checkout I wanted without using a cart platform.
Step 7: Create the thank-you page
After payment, Stripe sends the customer to your return URL:
body.append(
"return_url",
"https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}"
);
For a simple digital product, the thank-you page can include:
- PDF download link
- Google Doc copy link
- instructions for how to use the product
- next step or upsell
For example:
<h1>Your guide is ready.</h1>
<p>Download the PDF, then open the editable Google Doc if you want to work through the prompts directly.</p>
<a href="YOUR_PDF_LINK">Download the PDF</a>
<a href="YOUR_GOOGLE_DOC_COPY_LINK">Open the Google Doc</a>
For a small $7 product, this is enough to ship.
Later, you can make it more secure by verifying the Stripe session before showing download links.
What this unlocks
This changed how I look at Squarespace.
Before, I kept asking:
Can Squarespace do this?
Now I think:
Can Squarespace display the page while a Cloudflare Worker handles the logic?
That opens up a lot.
You can use the same pattern for:
- embedded checkout
- order bumps
- discount codes
- referral tracking
- affiliate tracking
- custom thank-you pages
- simple customer portals
- AI tools on your own site
- dynamic lead magnets
- quiz results
- gated downloads
- Stripe metadata
- webhook-based fulfillment
That is a strong combination.
Final notes
This is not the only way to do it.
Stripe Payment Links and Stripe Buy Buttons are easier.
But I wanted the checkout to live on the page.
I wanted something that felt closer to a custom checkout tool, without paying monthly for another platform.
This setup gave me that.
And once the first version worked, I could immediately see the bigger opportunity:
Landing page
→ embedded Stripe checkout
→ optional workshop add-on
→ thank-you page
→ audit or consulting offer
That is a real funnel.
Built on Squarespace.
Powered by Stripe.
Held together by a tiny Cloudflare Worker.