Edit on val.town ↗

Stripe webhooks

Stripe allows you to accept credit card payments online.

There are a two parts to a payment flow:

  1. Link to Stripe – User clicks on a link in your site that takes them to a Stripe checkout page.

  2. Handle Fulfillment – You can have a webhook that listens for successful payment events from Stripe. You can use this webhook to update your database to authorize the user to view premium content or send a confirmation email to the user.

This guide covers the two simplest ways to link your users to Stripe:

Stripe Payment Links are the simplest way to link to Stripe.

Docs: Stripe Payment Links

Create a Stripe Payment Link

Stripe Payment links do not require a backend. Link to them from HTML:

<a href="https://buy.stripe.com/test_fZe6rigkxdR9aze289">Buy</a>

When your users click on the link, they will be taken to a Stripe checkout page where they can enter their credit card information.

Stripe Checkout is a more customizable way to link to Stripe. You first create a Checkout Session on your server, then redirect the user to that session's URL.

Docs: Stripe Checkout

We ported Stripe Checkout Quickstart to Val Town: https://val.town/x/templates/stripe-checkout-quickstart

// @ts-ignore
import { Stripe } from "npm:stripe";

const stripe = new Stripe(Deno.env.get("STRIPE_TEST_API_KEY"));

export default async function (req: Request): Promise<Response> {
  const url = new URL(req.url);
  if (url.pathname === "/") {
    return new Response(
      `<h1>Stripe Checkout Demo</h1>
      <form action="/create-checkout-session" method="POST">
      <input name="name" placeholder="t shirt">
      <input type="number" name="price" placeholder="$5">
      <button type="submit">Buy</button>
      </form>`,
      { headers: { "content-type": "text/html" } }
    );
  }
  if (url.pathname === "/create-checkout-session" && req.method === "POST") {
    const form = await req.formData();
    const name = form.get("name");
    const price = form.get("price");
    const session = await stripe.checkout.sessions.create({
      line_items: [
        {
          price_data: {
            currency: "usd",
            product_data: {
              name,
            },
            unit_amount: Number(price) * 100,
          },
          quantity: 1,
        },
      ],
      mode: "payment",
      success_url: `https://${url.host}/confirmation`,
      cancel_url: `https://${url.host}`,
      automatic_tax: { enabled: true },
    });
    return new Response(null, {
      status: 303,
      headers: {
        Location: session.url,
      },
    });
  }

  if (url.pathname === "/confirmation") {
    return new Response(`<h1>You paid!</h1>`, {
      headers: { "content-type": "text/html" },
    });
  }
}

After a user pays, you may need to fulfill their order:

You can listen for events from Stripe to know when a payment has been successful. You can do this by setting up a webhook. A webhook is a URL on your server that Stripe will send events to.

Docs: Stripe Webhooks

Val Town makes it very easy to create a webhook using an HTTP trigger. Take a look at the example below to get started.

This webhook listens for new subscribers to Val Town Pro, and sends the Val Town team a Discord notification. The val itself (link below) explains how to set it up.

https://www.val.town/v/stevekrouse/newStripeSubscriber

import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
import Stripe from "npm:stripe";

const stripe = new Stripe(
  Deno.env.get("stripe_sk_customer_readonly") as string,
  {
    apiVersion: "2020-08-27",
  }
);

function getStripeCustomer(customerId: string) {
  return stripe.customers.retrieve(customerId);
}

export let newStripeEvent = async (req: Request) => {
  const url = new URL(req.url);
  if (req.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }

  const signature = req.headers.get("Stripe-Signature");
  const body = await req.text();
  const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
  let event;
  try {
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      webhookSecret
    );
  } catch (err: any) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  console.log(event);

  const newSubscription =
    event.data.previous_attributes.status === "incomplete" &&
    event.data.object.status === "active";

  if (!newSubscription) {
    console.log("Not a new subscription");
    console.log(
      event.data.previous_attributes.status,
      event.data.object.status
    );
    return Response.json("ok");
  }

  const customer = await getStripeCustomer(event.data.object.customer);
  const customerEmail = customer.email;

  console.log("New subscription", customerEmail);
  discordWebhook({
    url: Deno.env.get("discordUserEvents"),
    content: `New Subscription 🥳 ${customerEmail}`,
  });

  return Response.json("ok");
};