Accept a payment

The ordinary case: charge a card now, and find out when it worked.

The shape of it

StepWhat happens
You create a payment intentWe record what you intend to charge.
You create a checkout sessionYou get a URL.
Your customer paysOn a page hosted by the bank. You never see the card.
We verify with the bankWe re-read the payment rather than trusting a notification.
We send you a webhookpayment_intent.succeeded. Fulfil the order here.

Creating the payment

ts
const intent = await sp.paymentIntents.create({
  amount: 6000,
  currency: "usd",

  // Reaches the bank's own record, and is what you will see in their portal.
  // Lead with your own reference so you can find it again.
  description: "SCU-B0161 — Blue Hole Two-Tank",

  // Your identifiers. The payments list can be filtered by these.
  metadata: { shop_id: "scuba-sensation", booking_id: "SCU-B0161" },

  // Optional: the bank emails a receipt to this address.
  receipt_email: "diver@example.com",
});
description is the only field that reaches the bank’s own records, and it is frozen when the payment is taken. Put the reference you would search for first — a bare noun like “Booking” makes a bank statement impossible to reconcile.

Sending the customer

ts
const session = await sp.checkoutSessions.create({
  payment_intent: intent.id,
  success_url: "https://example.com/thanks",
  cancel_url: "https://example.com/cart",
  line_items: [{ name: "Blue Hole Two-Tank Dive", amount: 6000, quantity: 1 }],
});

redirect(session.url!);

line_items are shown on the checkout page so the customer can see what they are paying for. The payment’s own amount is what is charged — the line items are never summed.

Knowing it worked

Fulfil on the webhook, not on the redirect. A customer can close the tab after paying, and a redirect can be replayed by anyone who sees the URL.

ts
if (event.type === "payment_intent.succeeded") {
  const payment = event.data.object;
  await fulfil(payment.metadata.order_id);
}

If you also want to show the customer a result immediately, read the payment back by id when they land on your success_url. That is a display decision; the webhook is the record.