Send a handwritten thank-you card from a Stripe payment

When a customer pays through Stripe Checkout, mail them a card written with a real pen. One webhook handler, one API call.

What you need

  • Stripe Checkout with address collection turned on, so customer_details.address is filled in.
  • A webhook endpoint subscribed to checkout.session.completed.
  • A notecardHQ API key in NOTECARDHQ_API_KEY.

The handler

import express from "express";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();

app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, req.headers["stripe-signature"], process.env.STRIPE_WEBHOOK_SECRET,
    );
  } catch (err) {
    return res.status(400).send(`Webhook error: ${err.message}`);
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    const a = session.customer_details?.address;
    const [firstName, ...rest] = (session.customer_details?.name ?? "").split(" ");
    if (a?.line1 && firstName) {
      const r = await fetch("https://you.notecardhq.com/api/v1/notes", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.NOTECARDHQ_API_KEY}`,
          "Content-Type": "application/json",
          // Stripe may deliver an event more than once; the same key never sends twice.
          "Idempotency-Key": `checkout:${session.id}`,
        },
        body: JSON.stringify({
          externalId: session.customer ?? session.id,
          recipient: {
            firstName, lastName: rest.join(" "),
            address1: a.line1, address2: a.line2 ?? "",
            city: a.city, state: a.state ?? "", postalCode: a.postal_code ?? "",
            country: a.country,
          },
          content: `Hi ${firstName},\n\nThank you for your order. It means a lot to a small team.\n\nThe team`,
          context: { stripe_session: session.id },
        }),
      });
      // Ask Stripe to retry only on temporary failures. 409 and 422 need a person.
      if (r.status === 429 || r.status >= 500) return res.status(503).end();
      if (!r.ok) console.error("notecardHQ", r.status, await r.text());
    }
  }
  res.json({ received: true });
});

app.listen(3000);

Why it is safe to retry

Stripe retries webhooks that fail, and can deliver the same event twice. Because the Idempotency-Key is built from the Checkout Session id, every delivery of that event maps to the same note. The first request creates it; every repeat returns it.

Only first orders

To thank a customer once, not on every purchase, key the note on the customer instead: first-order:${session.customer}. Later payments by the same customer reuse the key, and notecardHQ answers with the original note (or 409 if the body changed), so no second card goes out.

Field rules, statuses and errors are in the API reference. Prefer another language? See the quickstart.

Send your first note.

Tell us what you want to send and we will set up your workspace.