# Checkout with Stripe

> Take payment on your own website with Stripe's Payment Element, using the client secret and publishable key from checkout, send the shopper to their order page, and know what happens when they never finish.

Source: https://docs.bifrostorbit.com/commerce/developers/checkout-with-stripe/

Orders are paid into the shop's own Stripe account. Your website shows Stripe's Payment Element with keys Commerce Orbit gives you; card details go from the shopper's browser to Stripe and never reach your server or ours. You don't need a Stripe account or keys of your own.

## The flow

1. Build the cart with the [storefront API](https://docs.bifrostorbit.com/commerce/developers/storefront-api/) until it has lines, an email, a name, a shipping choice and, for delivery, an address, and `problems` is empty.
2. Call checkout with `{ "method": "card" }`. Commerce Orbit prices the order on the server, creates it, takes the stock and creates a Stripe PaymentIntent for exactly the order total.
3. Show the Payment Element with the `clientSecret` and `publishableKey` from the answer.
4. Confirm the payment with Stripe, with a `return_url` on your website.
5. Stripe tells Commerce Orbit when the payment succeeds. The order becomes paid, the shopper gets the confirmation email and the shop's team is told.
6. On the return page, read the order to show what happened.

The amount is always worked out on the server from the cart, so the browser can't change what is charged.

## Show the Payment Element

```js
import { loadStripe } from "@stripe/stripe-js";

const base = "https://app.bifrostorbit.com/commerce/api/public/northwilds";
const token = localStorage.getItem("cart");

const response = await fetch(`${base}/carts/${token}/checkout`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ method: "card" }),
});
const { data, error } = await response.json();
if (error) throw new Error(error.message);

const stripe = await loadStripe(data.payment.publishableKey);
const elements = stripe.elements({ clientSecret: data.payment.clientSecret });
elements.create("payment").mount("#payment");

document.querySelector("#pay").addEventListener("click", async () => {
  const email = "ingrid@northwilds.com";
  const { error } = await stripe.confirmPayment({
    elements,
    confirmParams: { return_url: `https://northwilds.com/order/${data.reference}?email=${encodeURIComponent(email)}` },
  });
  if (error) showMessage(error.message);
});
```

Load Stripe with the `publishableKey` from each checkout answer rather than a key copied into your code: the shop can move from test to live keys, or change accounts, without you changing anything.

The Payment Element shows the payment methods switched on in the shop's Stripe account, such as cards, Apple Pay, Google Pay or Klarna. Some of them, and 3D Secure, send the shopper away and back to your `return_url`.

Calling checkout from the browser only works from the shop's allowed websites. See [The storefront API](https://docs.bifrostorbit.com/commerce/developers/storefront-api/#which-websites-can-call-it).

## The return page

Stripe sends the shopper back to your `return_url` with `payment_intent`, `payment_intent_client_secret` and `redirect_status` added. Read the order with the reference and email:

```bash
curl 'https://app.bifrostorbit.com/commerce/api/public/northwilds/orders/NW1042?email=ingrid%40northwilds.com'
```

If Stripe's message hasn't arrived yet, this call asks Stripe itself, so `paymentStatus` is `paid` as soon as the payment went through. Show `pending` as "We're confirming your payment" and read again after a few seconds. The [hosted shop](https://docs.bifrostorbit.com/commerce/everyone/hosted-shop/)'s order page works the same way, so you can also use it as the `return_url`:

```
https://app.bifrostorbit.com/commerce/shop/northwilds/orders/NW1042?email=ingrid%40northwilds.com
```

## Orders on invoice

For a shopper whose company has invoice terms (the cart's `company.terms` is `invoice` and `company.onStop` is `false`), call checkout with `{ "method": "invoice" }`. The answer has `"status": "placed"` and no payment: the order is placed at once and the invoice is emailed. Go straight to the order page.

## Webhooks and duplicates

You don't handle Stripe's webhooks: Commerce Orbit does, with a webhook it created in the shop's Stripe account. It checks each message's signature and handles each Stripe event once, however often Stripe sends it. The PaymentIntent is created with an idempotency key from the order, so a retried checkout request never charges twice for the same order.

To hear about orders on your own systems, see [Events](https://docs.bifrostorbit.com/commerce/developers/webhooks-and-events/).

## When things go wrong

- **The payment fails:** Stripe shows the reason in the Payment Element, the order becomes `failed`, and the shopper can try again with the same `clientSecret`. Don't call checkout again for that.
- **The shopper changes the cart:** call checkout again. The unpaid order is cancelled, its stock released, and a new order and PaymentIntent are made for the new total.
- **The shopper never finishes:** the order stays `pending` while Commerce Orbit asks Stripe about it every minute. Once the shop's stock reservation time has passed (**Hold stock for** in the shop settings, 15 minutes unless the shop changes it, at most 120) it is cancelled and its stock goes back on sale. Nothing was charged.
- **A payment arrives after that:** the order stays cancelled and its history says it was paid after it was cancelled, so the shop's team can refund it in Stripe.
- **The shop's Stripe key stops working:** checkout answers `409` with "The payment couldn't start: …" and Stripe's reason, and nothing is created. Without any connection it answers "This shop can't take card payments yet".

## Test mode

When the shop is connected with Stripe test keys, use Stripe's test cards, such as `4242 4242 4242 4242` with any future date and CVC, and `4000 0025 0000 3155` to try 3D Secure. No money moves. Ask the shop's team to cancel test orders before going live.
