Stripe Checkout is Stripe's hosted payment page. Instead of building a custom payment form, you create a Checkout Session and redirect your customer to Stripe's hosted URL. Stripe handles the payment UI, card validation, 3D Secure authentication, and more.

Checkout supports three modes:

  • payment — one-time payment
  • subscription — recurring billing
  • setup — save a payment method without charging

Payment Mode

Use "mode" => "payment" for one-time purchases. You define the products and quantities using line_items, each referencing a Price object you've created in Stripe.

{:ok, session} = LatticeStripe.Checkout.Session.create(client, %{
  "mode" => "payment",
  "success_url" => "https://example.com/success?session_id={CHECKOUT_SESSION_ID}",
  "cancel_url" => "https://example.com/cancel",
  "line_items" => [
    %{"price" => "price_1OzqKZ2eZvKYlo2CHQkCGK7T", "quantity" => 1}
  ]
})

# Redirect the customer to session.url — the Stripe-hosted payment page
IO.puts("Redirect to: #{session.url}")

The {CHECKOUT_SESSION_ID} template in success_url is replaced by Stripe with the actual session ID when the customer is redirected back. Use it to retrieve the completed session:

def handle_success(conn, %{"session_id" => session_id}) do
  {:ok, session} = LatticeStripe.Checkout.Session.retrieve(client, session_id)
  fulfill_order(session)
  render(conn, :success)
end

Important: Don't rely on the redirect to confirm payment. Use webhooks to listen for checkout.session.completed — the redirect can be skipped or manipulated. See Webhooks for setup.

Ad-Hoc Line Items (Price Data)

If you don't have pre-created Price objects, you can define line items inline using price_data:

{:ok, session} = LatticeStripe.Checkout.Session.create(client, %{
  "mode" => "payment",
  "success_url" => "https://example.com/success",
  "cancel_url" => "https://example.com/cancel",
  "line_items" => [
    %{
      "price_data" => %{
        "currency" => "usd",
        "product_data" => %{"name" => "LatticeStripe Pro License"},
        "unit_amount" => 4900
      },
      "quantity" => 1
    }
  ]
})

Pre-Filling Customer Information

Pass an existing customer ID to pre-fill their email and saved payment methods:

{:ok, session} = LatticeStripe.Checkout.Session.create(client, %{
  "mode" => "payment",
  "customer" => "cus_OtVFqSomeStripeId",
  "success_url" => "https://example.com/success",
  "cancel_url" => "https://example.com/cancel",
  "line_items" => [
    %{"price" => "price_...", "quantity" => 1}
  ]
})

Subscription Mode

Use "mode" => "subscription" to set up recurring billing. The line items should reference prices with recurring intervals:

{:ok, session} = LatticeStripe.Checkout.Session.create(client, %{
  "mode" => "subscription",
  "success_url" => "https://example.com/success?session_id={CHECKOUT_SESSION_ID}",
  "cancel_url" => "https://example.com/cancel",
  "line_items" => [
    %{
      "price" => "price_1OzqKZ2eZvKYlo2CHmonthly",  # A recurring price object
      "quantity" => 1
    }
  ]
})

After a successful checkout, Stripe creates a Subscription and a recurring PaymentIntent. Listen for the checkout.session.completed webhook to provision access, then customer.subscription.updated and invoice.payment_succeeded to manage ongoing billing.

Subscription with Trial

{:ok, session} = LatticeStripe.Checkout.Session.create(client, %{
  "mode" => "subscription",
  "success_url" => "https://example.com/success",
  "cancel_url" => "https://example.com/cancel",
  "subscription_data" => %{
    "trial_period_days" => 14
  },
  "line_items" => [
    %{"price" => "price_monthly", "quantity" => 1}
  ]
})

Setup Mode

Use "mode" => "setup" to collect and save a customer's payment method without charging them immediately. This is useful for:

  • Setting up a payment method before a subscription starts
  • Adding a backup payment method
  • Capturing payment details for future invoicing
{:ok, session} = LatticeStripe.Checkout.Session.create(client, %{
  "mode" => "setup",
  "success_url" => "https://example.com/payment-method-saved",
  "cancel_url" => "https://example.com/cancel",
  "customer" => "cus_OtVFqSomeStripeId"
})

After setup, the checkout.session.completed webhook contains a setup_intent field with the ID of the resulting SetupIntent. Use it to retrieve the saved PaymentMethod:

def handle_setup_complete(event) do
  session = event.data["object"]
  setup_intent_id = session["setup_intent"]

  {:ok, setup_intent} = LatticeStripe.SetupIntent.retrieve(client, setup_intent_id)
  payment_method_id = setup_intent.payment_method

  # Attach this payment method to the customer and set as default
  LatticeStripe.PaymentMethod.attach(client, payment_method_id, %{
    "customer" => session["customer"]
  })
end

Retrieving Sessions

Retrieve a session by ID to check its status and access payment details:

{:ok, session} = LatticeStripe.Checkout.Session.retrieve(client, "cs_test_...")
IO.puts("Status: #{session.status}")
IO.puts("Payment status: #{session.payment_status}")

Expanding Fields

Some fields on a Checkout Session require explicit expansion. For example, to get line items inline (rather than making a separate request):

{:ok, session} = LatticeStripe.Checkout.Session.retrieve(client, "cs_test_...",
  expand: ["line_items"]
)

# session.line_items is now populated

Listing Sessions

{:ok, resp} = LatticeStripe.Checkout.Session.list(client, %{
  "limit" => 20,
  "customer" => "cus_OtVFqSomeStripeId"
})

sessions = resp.data.data
IO.puts("Found #{length(sessions)} sessions")

Auto-Pagination with Streams

For processing large numbers of sessions:

client
|> LatticeStripe.Checkout.Session.stream!(%{"created" => %{"gte" => thirty_days_ago}})
|> Stream.filter(fn s -> s.payment_status == "paid" end)
|> Enum.each(&send_receipt/1)

Expiring Sessions

A Checkout Session stays open for 24 hours by default. If you need to cancel an open session before it expires — for example, when a user abandons it or an item goes out of stock — call Session.expire/3:

{:ok, expired} = LatticeStripe.Checkout.Session.expire(client, "cs_test_...")
IO.puts("Status: #{expired.status}")
# Status: expired

Expired sessions cannot be reopened. Create a new session if you need to retry.

Listing Line Items

Retrieve the line items for a completed session — useful for building receipts or order summaries:

{:ok, resp} = LatticeStripe.Checkout.Session.list_line_items(client, session.id)
items = resp.data.data  # [%LatticeStripe.Checkout.LineItem{}, ...]

Enum.each(items, fn item ->
  IO.puts("#{item.description}: #{item.amount_total} #{item.currency} x#{item.quantity}")
end)

For large numbers of line items, use the stream variant:

client
|> LatticeStripe.Checkout.Session.stream_line_items!(session.id)
|> Enum.each(&process_line_item/1)

Common Pitfalls

success_url and cancel_url are required for all modes. Omitting them produces an ArgumentError before the request is even sent. Both must be absolute URLs (starting with https://).

mode must be "payment", "subscription", or "setup" — validated pre-network. LatticeStripe validates the mode parameter before making any HTTP request. An invalid or missing mode raises ArgumentError immediately — you won't see this as a Stripe API error.

Session URLs expire after 24 hours. The session.url is only valid for 24 hours. Don't cache it. If a customer clicks a stale link, redirect them through your checkout flow again to create a fresh session.

Use webhooks to confirm payment completion — don't rely on the redirect. Customers can close their browser before being redirected to success_url, or the redirect URL can fail. The only reliable way to know a payment succeeded is the checkout.session.completed webhook event. See Webhooks.

Subscription mode requires a recurring price, not a one-time price. If you use a one-time price with "mode" => "subscription", Stripe returns a 400 error. Create a Price with "recurring" => %{"interval" => "month"} for subscription line items.