Programování

Stripe Checkout v Node.js: platba kartou, webhook a testování

Stripe Checkout krok za krokem: vytvoření Checkout Session, přesměrování, ověření webhooku, idempotentní vyřízení objednávky a testování.

12. 10. 2026 · 4 min čtení

Stripe Checkout je hostovaná platební stránka Stripe. Nemusíte řešit formulář na kartu, 3D Secure ani PCI DSS – zákazníka přesměrujete na Stripe a ten vám po zaplacení dá vědět. Ukážeme kompletní integraci v Node.js s Expressem, včetně části, kterou mnoho tutoriálů vynechá: spolehlivého potvrzení platby webhookem.

Jak tok vypadá

  1. Zákazník klikne na „Zaplatit“.
  2. Váš server vytvoří Checkout Session a vrátí její URL.
  3. Prohlížeč přesměruje zákazníka na Stripe, ten zaplatí.
  4. Stripe přesměruje zpět na vaši success_url.
  5. Stripe pošle webhook checkout.session.completed – teprve ten je spolehlivý důkaz platby.

Krok 4 nesmí nic aktivovat. Zákazník může zavřít prohlížeč před přesměrováním, nebo naopak URL ručně otevřít bez placení.

Příprava

  • Účet na Stripe a testovací klíče z Dashboardu (začínají sk_test_).
  • Node.js 18+ a balíček stripe:
npm install stripe express
export STRIPE_SECRET_KEY="sk_test_..."
export STRIPE_WEBHOOK_SECRET="whsec_..."   # získáte níže

Tajný klíč nikdy nedávejte do frontendu ani do repozitáře.

Vytvoření Checkout Session

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

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

app.post("/checkout", express.json(), async (req, res) => {
  // Cenu a položky vždy určuje server, nikdy nevěřte ceně z prohlížeče.
  const order = await db.orders.createPending({ userId: req.user.id, productId: "ebook-sql" });

  const session = await stripe.checkout.sessions.create(
    {
      mode: "payment",
      line_items: [
        {
          price_data: {
            currency: "czk",
            product_data: { name: "E-book: SQL v praxi" },
            unit_amount: 29900, // v haléřích = 299,00 Kč
          },
          quantity: 1,
        },
      ],
      customer_email: req.user.email,
      client_reference_id: String(order.id),
      metadata: { orderId: String(order.id) },
      success_url: "https://example.cz/dekujeme?session_id={CHECKOUT_SESSION_ID}",
      cancel_url: "https://example.cz/kosik",
    },
    { idempotencyKey: `checkout-order-${order.id}` }
  );

  await db.orders.setStripeSession(order.id, session.id);
  res.json({ url: session.url });
});

Důležité detaily:

  • unit_amount je v nejmenší jednotce měny – u CZK v haléřích.
  • {CHECKOUT_SESSION_ID} doplní Stripe sám, nepřepisujte ho.
  • metadata a client_reference_id propojí platbu s vaší objednávkou.
  • Pro opakovaně prodávané produkty je čistší vytvořit v Dashboardu Price a předat price: "price_..." místo price_data.
  • Pro předplatné použijte mode: "subscription" s opakující se cenou.

Na frontendu stačí:

const res = await fetch("/checkout", { method: "POST" });
const { url } = await res.json();
window.location.href = url;

Webhook

Stripe podepisuje webhooky. Ověření potřebuje raw tělo požadavku, proto route registrujte s express.raw() a mimo globální express.json():

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(`Neplatný podpis: ${err.message}`);
    }

    // Deduplikace – Stripe může stejnou událost poslat víckrát.
    const isNew = await db.webhookEvents.insertIfNew(event.id, event.type);
    if (!isNew) return res.sendStatus(200);

    switch (event.type) {
      case "checkout.session.completed":
      case "checkout.session.async_payment_succeeded": {
        const session = event.data.object;
        if (session.payment_status === "paid") {
          await fulfillOrder(session.metadata.orderId, session.id);
        }
        break;
      }
      case "checkout.session.async_payment_failed":
        await db.orders.markFailed(event.data.object.metadata.orderId);
        break;
    }

    res.sendStatus(200);
  }
);

constructEvent ověří podpis i časové razítko (výchozí tolerance 5 minut). payment_status kontrolujeme proto, že u odložených platebních metod přijde checkout.session.completed dřív, než jsou peníze potvrzené – zaplacení pak oznámí až async_payment_succeeded.

Idempotentní vyřízení

async function fulfillOrder(orderId, sessionId) {
  // Atomický přechod stavu: vyřídí se jen jednou, i při souběžných webhoocích.
  const updated = await db.query(
    "UPDATE orders SET status = 'paid', paid_at = now() WHERE id = $1 AND status = 'pending' RETURNING id",
    [orderId]
  );
  if (updated.rowCount === 0) return;
  await emailQueue.add("send-ebook", { orderId });
}

Webhook má odpovědět rychle. Odeslání e-mailu a další práci dejte do fronty.

Lokální testování se Stripe CLI

stripe login
stripe listen --forward-to localhost:3000/webhooks/stripe

Příkaz vypíše webhook secret whsec_... pro lokální vývoj – nastavte ho do STRIPE_WEBHOOK_SECRET. V dalším terminálu můžete události vyvolat:

stripe trigger checkout.session.completed

Nebo projděte celý tok v prohlížeči s testovacími kartami:

Číslo karty Výsledek
4242 4242 4242 4242 úspěšná platba
4000 0025 0000 3155 vyžaduje 3D Secure ověření
4000 0000 0000 9995 zamítnutí (nedostatek prostředků)

Platnost libovolné budoucí datum, CVC libovolné tři číslice.

Stránka „Děkujeme“

Na success_url můžete session načíst a zobrazit stav, ale nic neaktivujte:

const session = await stripe.checkout.sessions.retrieve(req.query.session_id);

Pokud webhook ještě nedorazil, zobrazte „Platba se zpracovává“ a stav kontrolujte z vlastní databáze.

Přechod do produkce

  • Vyměňte testovací klíče za živé (sk_live_).
  • V Dashboardu v sekci Webhooks přidejte produkční endpoint a vyberte jen potřebné události. Každý endpoint má vlastní whsec_ secret.
  • Zkontrolujte, že endpoint běží na HTTPS a nestojí za přihlášením nebo CSRF ochranou.
  • Sledujte v Dashboardu neúspěšná doručení webhooků – Stripe je opakuje, ale jen omezenou dobu.

Obecné zásady pro příjem webhooků od jakékoli služby shrnuje článek webhooky bezpečně.