How I Built My Own Embedded Stripe Checkout on a Squarespace Page

Quick note: this is more advanced than a normal Squarespace code snippet. You’ll be working with Stripe keys, a Cloudflare Worker, and a little custom JavaScript. Start in test mode before using this with real payments.

I wanted a checkout form that lived directly on my Squarespace landing page.

No redirect.

No third-party cart page.

No awkward handoff.

Just read the page, enter your card, and get the thing.

Squarespace is great for pages, design, content, and getting something online without turning every idea into a full software project.

But checkout can get limiting when you want something custom.

You can use Squarespace Commerce. You can use Stripe Payment Links. You can use tools like SamCart, ThriveCart, SendOwl, Lemon Squeezy, or any number of checkout platforms.

Those are all valid options.

I just wanted to see if I could build my own embedded checkout using Squarespace, Stripe, and a small Cloudflare Worker.

And it worked.

This tutorial walks through the setup.

Quick note: this is more advanced than a normal Squarespace code snippet. You’ll be working with Stripe keys, a Cloudflare Worker, and a little custom JavaScript. Start in test mode before using this with real payments.

You can see the end result here:

[DEMO LINK]


What we’re building

We’re going to add an embedded Stripe Checkout form directly inside a Squarespace page.

The page still lives in Squarespace.

Stripe handles the payment.

Cloudflare Workers handles the secure backend logic that Squarespace cannot handle on its own.

Here’s the general flow:

  1. Someone lands on your Squarespace page.
  2. The page loads Stripe.
  3. The page asks your Cloudflare Worker to create a checkout session.
  4. The Worker talks to Stripe securely.
  5. Stripe sends back a client secret.
  6. The checkout form appears on the Squarespace page.
  7. The customer pays.
  8. Stripe sends them to your thank-you page.

So Squarespace becomes the page layer.

Cloudflare Workers becomes the logic layer.

Stripe becomes the money layer.

That’s the part that made this click for me.


Why you need the Cloudflare Worker

The main issue is that Squarespace does not give you server-side access.

That matters because Stripe has two kinds of keys.

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 and make real requests to your Stripe account.

If you put that secret key inside a Squarespace Code Block, someone 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 to create a checkout session. The Worker talks to Stripe using your secret key. Then Stripe sends back what the page needs to display the embedded checkout.


What you need before starting

  • A Stripe account
  • A Stripe product
  • A Stripe price ID
  • 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 something like this:

price_123456789

Make sure you use the Price ID, not the Product ID.


Step 1: Create your Cloudflare Worker

In Cloudflare, create a new Worker.

This Worker will create the Stripe Checkout Session.

Paste this code into the Worker:

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");

        body.append("allow_promotion_codes", "true");

        body.append(
          "return_url",
          "https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}"
        );

        body.append("line_items[0][price]", env.STRIPE_GUIDE_PRICE_ID);
        body.append("line_items[0][quantity]", "1");

        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",
    },
  });
}

Before you deploy it, update your thank-you page URL:

"https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}"

Replace that with your real thank-you page.

Then update the allowed origins:

const allowedOrigins = [
  "https://yourdomain.com",
  "https://www.yourdomain.com"
];

Replace those with your actual domain.

Do not paste your Stripe secret key directly into the Worker code.

We’ll add that separately.


Step 2: Add your Worker variables

In Cloudflare, go to your Worker settings and add your variables/secrets.

You need:

STRIPE_SECRET_KEY=sk_live_...
STRIPE_GUIDE_PRICE_ID=price_...

The first one is your Stripe secret key.

The second one is the Stripe Price ID for the product you want to sell.

If you want to include an optional add-on, you can also add:

STRIPE_WORKSHOP_PRICE_ID=price_...

That part is optional.

For my setup, I used a $7 guide as the main product and a $17 workshop as the optional add-on.

If you do not want an add-on, leave out the workshop price ID.


Step 3: Add the checkout to Squarespace

Now go to your Squarespace page.

Add a Code Block where you want the checkout to appear.

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>

Then replace this:

pk_live_YOUR_PUBLISHABLE_KEY

with your Stripe publishable key.

And replace this:

https://YOUR_WORKER_URL/create-checkout-session

with your actual Cloudflare 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.

Your Squarespace code needs to call the same endpoint your Worker is listening for.

For example, if your Squarespace code calls:

/create-checkout-session

your Worker needs to accept:

/create-checkout-session

If the page calls one path and the Worker is listening for another, the checkout may stay stuck on “Loading checkout…”

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.

You still need to create the actual coupon and promotion code inside Stripe.

For example:

Code: EARLY
Discount: 50% off

Once that exists in Stripe, customers can enter the code during checkout.


Step 6: Add an optional add-on

This part is optional.

I wanted to sell a $17 workshop alongside a $7 guide.

That is what this code does:

if (env.STRIPE_WORKSHOP_PRICE_ID) {
  body.append("optional_items[0][price]", env.STRIPE_WORKSHOP_PRICE_ID);
  body.append("optional_items[0][quantity]", "1");
}

If you add STRIPE_WORKSHOP_PRICE_ID as a Worker variable, Stripe will include the optional item.

If you leave that variable out, the checkout will only show the main product.

This was the closest version of an “order bump” style checkout I wanted without using a full cart platform.


Step 7: Create your thank-you page

After payment, Stripe sends the customer to the return URL you added in your Worker:

body.append(
  "return_url",
  "https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}"
);

For a simple digital product, your thank-you page can include:

  • PDF download link
  • Google Doc copy link
  • instructions for using the product
  • a next step or related offer

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 product, this may be enough to ship.

Later, you can make it more secure by verifying the Stripe session before showing download links.


If something breaks

The checkout stays stuck on “Loading checkout…”

Make sure the endpoint in your Squarespace Code Block matches the endpoint in your Worker.

Also make sure your Worker is deployed.

You see a CORS error

Make sure your allowed origins include your real domain.

const allowedOrigins = [
  "https://yourdomain.com",
  "https://www.yourdomain.com"
];

Use your actual domain, including https.

Stripe says the price is missing

Make sure you added the right Worker variable:

STRIPE_GUIDE_PRICE_ID=price_...

Also make sure you used the Stripe Price ID, not the Product ID.

The checkout loads, but the payment does not work

Check whether you are using live keys or test keys.

Your publishable key and secret key need to be from the same mode.

Use test keys while testing.

Use live keys when you are ready to sell for real.


What this changed for me

The checkout itself is useful.

But the bigger shift is the pattern.

Before, I kept asking:

Can Squarespace do this?

Now I think:

Can Squarespace display the page while something lightweight handles the logic?

That opens up a different way of thinking about what can be built on a Squarespace site.

You could use the same general pattern for things like:

  • 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

Squarespace does not need to do everything.

It can handle the page.

Cloudflare Workers can handle the logic.

Stripe can handle the money.


Final notes

This is not the easiest way to take payments on Squarespace.

Stripe Payment Links and Stripe Buy Buttons are much simpler.

If all you need is a basic payment button, start there.

But I wanted the checkout to live directly on the page.

I wanted something that felt closer to a custom checkout experience without paying monthly for another platform.

This setup gave me that.

Once I saw it working, the checkout itself became less interesting than the pattern.

Maybe there are ideas you have been sitting on because you assumed Squarespace could not do them.

A custom calculator.

A gated download.

A tiny app.

A better sales flow.

A client portal.

A dynamic lead magnet.

Maybe Squarespace does not need to do the whole thing.

Maybe it just needs to display the experience, while something lightweight handles the logic behind the scenes.

Omari Harebin

Omari Harebin is the founder of SQSPThemes. He helps Squarespace designers and product sellers find the market moment inside their existing work, then turn it into articles, offers, demos, and assets that compound.

Start here: Book a free clarity call to get a read on what you already have →

https://www.omariharebin.com