Back to blog
How to add Stripe to Astro
6 min read

In this tutorial, I’ll show you how to set up Stripe in an Astro project, from creating a new project to handling webhooks and creating checkout sessions.

Step 1: Creating a New Astro Project

To get started, we need to create a new Astro project using the following command:

npm create astro@latest

The version of Astro being used is 4.16.5

Step 2: Installing Stripe

Next, install the Stripe package.

npm i stripe

The version of Stripe being used is 17.2.0

Step 3: Configuring Stripe

Create a file called src/lib/stripe.ts to initialize Stripe and handle its configuration:

import Stripe from 'stripe';

if (!import.meta.env.STRIPE_SECRET_KEY) {
    throw new Error('Missing Stripe secret key');
}

export const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY, {
    apiVersion: '2024-09-30.acacia',
});

Nothing fancy here, just the fact that the apiVersion is based on the stripe version ( for 17.2.0 is 2024-09-30.acacia )

After that create a new file in src/lib/get-prices.ts and add the following:

export async function getPrices() {
    const data = [
        {
            id: 1,
            amount: 1000,
            title: 'One time Price 1',
        },
        {
            id: 2,
            amount: 1000,
            title: 'One time Price 2',
        },
        {
            id: 3,
            amount: 1500,
            credits: 10,
            title: '10 credits',
        },
        {
            id: 4,
            amount: 3000,
            credits: 25,
            title: '25 credits',
        },
    ];
    return data;
}

Here, we will configure the prices.

Step 4: Setting Up Stripe CLI for Webhooks

Webhooks allow Stripe to notify your server of events (such as payment completions). To listen to these events on local we need the following:

"stripe:listen": "stripe listen --forward-to http://localhost:4321/api/webhooks/stripe"

You’ll also need to install the Stripe CLI, which allows your local server to receive Stripe events. More details on how to install the Stripe CLI can be found at https://docs.stripe.com/stripe-cli.

After that, run:

npm run stripe:listen

It might ask you to login and after that you should see a similar message:

Ready! You are using Stripe API Version. Your webhook signing secret is whsec_something

Step 5: Configuring Environment Variables

In the root of your project, create a .env file with the following content:

STRIPE_SECRET_KEY=your_secret_key_from_stripe
STRIPE_SIGNING_SECRET=signing_key_from_stripe_cli

Step 6: Adding Tailwind CSS and Node.js

For basic styling and to handle the backend requests, add Tailwind CSS and Node.js integrations to your project:

npx astro add tailwind
npx astro add node

Step 7: Creating Stripe Checkout Action

You can read more about actions in Astro at https://docs.astro.build/en/guides/actions/.

We will now create an action to handle the checkout process. Create a file at src/actions/index.ts with the following code:

import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:schema";
import { getPrices } from "../lib/get-prices";
import { stripe } from "../lib/stripe";

export const server = {
  createCheckout: defineAction({
    input: z.object({
      priceId: z.number(),
    }),
    accept: "form",
    handler: async (input) => {
      const prices = await getPrices();
      const price = prices.find((p) => p.id === input.priceId);

      if (!price) {
        throw new ActionError({
          code: "NOT_FOUND",
          message: "Price not found.",
        });
      }

      const baseUrl = 'http://localhost:4321'; // replace with your production URL

      const stripeSession = await stripe.checkout.sessions.create({
        mode: "payment",
        payment_method_types: ["card"],
        line_items: [
          {
            quantity: 1,
            price_data: {
              unit_amount: price.amount,
              currency: "usd",
              product_data: {
                name: price.title,
                description: `Buy ${price.title} product`,
              },
            },
          },
        ],
        metadata: {
          priceId: price.id,
        },
        success_url: `${baseUrl}/?stripe=success`,
        cancel_url: `${baseUrl}/?stripe=cancel`,
      });

      if (!stripeSession.url) {
        throw new ActionError({
          code: "NOT_FOUND",
          message: "Could not create Stripe session",
        });
      }

      return {
        url: stripeSession.url,
      };
    },
  }),
};

Here, we are just taking the priceId from the frontend, look for it inside our prices list. If we find it, we create a stripe checkout session and send the url to the frontend. For the stripe session, we will need to specify a success/cancel url, where the user should be redirected after the payment. Also, we have the possibility to add extra metadata that we will receive into our webhook. Here you usually add the priceId and userId.

Step 8: Rendering the Checkout Form

Now, let’s display pricing cards and integrate the checkout button. Add the following code to src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import { getPrices } from '../lib/get-prices';
import { actions } from 'astro:actions';

const prices = await getPrices();

const result = Astro.getActionResult(actions.createCheckout);
if (result && !result.error) {
	return Astro.redirect(result.data.url)
}
---

<Layout title="Welcome to Astro.">
	<h1 class="text-center text-5xl font-bold text-gray-200">Pricing</h1>
    <ul class="mt-12 grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-3 p-4">
        {
            prices.map((price) => (
                <li class="mx-auto w-full max-w-5xl space-y-4 rounded-lg bg-gray-900 p-8 text-white">
                    <h2 class="text-2xl font-bold">{price.title}</h2>
                    <p class="mt-4 text-3xl font-bold">${price.amount / 100}</p>
                    <form method="POST" action={actions.createCheckout}>
                        <input type="hidden" name="priceId" value={price.id} />
                        <button class="bg-blue-500 text-white hover:bg-blue-600 p-4">
                            Buy
                        </button>
                    </form>
                </li>
            ))
        }
    </ul>
</Layout>

Here, we are fetching the prices on the server, and we create cards for each of the price. Then, for each price, we have a form that calls the previously defined action in order to receive the stripe checkout session. After that, we redirect the user to the stripe page.

Step 9: Creating a Webhook for Stripe

Finally, handle Stripe webhook events. Create the file src/pages/api/webhooks/stripe.ts with the following code:

import type { APIRoute } from 'astro';
import type Stripe from 'stripe';
import { stripe } from '../../../lib/stripe';

type Metadata = {
    priceId: string;
};

export const POST: APIRoute = async ({ request }) => {
    const signature = request.headers.get('stripe-signature');
    if (!signature) {
        return new Response(JSON.stringify({ error: 'Invalid signature' }), {
            status: 400,
            headers: {
                'Content-Type': 'application/json',
            },
        });
    }

    const stripeSigningSecret = import.meta.env.STRIPE_SIGNING_SECRET as string;
    try {
        const event = stripe.webhooks.constructEvent(
            await request.text(),
            signature,
            stripeSigningSecret,
        );

        const completedEvent = event.data.object as Stripe.Checkout.Session & {
            metadata: Metadata;
        };

        if (event.type === 'checkout.session.completed') {
            console.log('Paid', completedEvent.amount_total);
            console.log('Metadata', completedEvent.metadata);

            // Update your database or user status here
        }
        return new Response(JSON.stringify({ success: true, error: null }), {
            status: 200,
            headers: {
                'Content-Type': 'application/json',
            },
        });
    } catch (err) {
        return new Response(
            JSON.stringify({
                success: false,
                error: (err as { message: string }).message,
            }),
            {
                status: 500,
                headers: {
                    'Content-Type': 'application/json',
                },
            },
        );
    }
};

This webhook listens for the checkout.session.completed event from Stripe. Upon receiving the event, you can update your database, apply changes to the user’s account, or trigger any other post-payment action.

Conclusion

That’s it! By following these steps, you can successfully integrate Stripe into your Astro project. Pretty easy right?