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)
endImportant: 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"]
})
endRetrieving 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 populatedListing 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: expiredExpired 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.