Forget clunky forms and delayed confirmations.
With Phoenix LiveView + Stripe, you can build a fast, real-time, and secure payment flow — with minimal JavaScript and maximum control.
Why Use LiveView for Payments?
- 🧠 Fewer moving parts (no full SPA)
- ⚡ Instant UI updates (no reloads)
- 🔁 Tight feedback loop (LiveView <-> Stripe <-> User)
- 🧩 Server-side logic in Elixir (easier to test and secure)
Step 1: Add stripe_elixir
In your mix.exs
:
defp deps do
[
{:stripe, "~> 3.0"},
{:finch, "~> 0.16"}
]
end
Configure your secret key:
# config/runtime.exs
config :stripity_stripe, api_key: System.fetch_env!("STRIPE_SECRET")
Step 2: Set Up Stripe Elements
Stripe handles sensitive card input via secure iframes.
Add their JS in your app.js
:
import { Socket } from "phoenix"
import topbar from "../vendor/topbar"
let stripe = Stripe("pk_test_...")
let Hooks = {}
Hooks.StripeCard = {
mounted() {
const elements = stripe.elements()
const card = elements.create("card")
card.mount(this.el)
this.card = card
this.handleEvent("confirm_payment", async ({ client_secret }) => {
const result = await stripe.confirmCardPayment(client_secret, {
payment_method: {
card: this.card,
}
})
this.pushEvent("payment_result", result)
})
}
}
Step 3: Render the Form with LiveView
<div phx-hook="StripeCard" id="card-element" class="p-4 border rounded"></div>
<form phx-submit="checkout">
<button type="submit" class="btn" <%= if @submitting, do: "disabled" %>>
<%= if @submitting, do: "Processing...", else: "Pay Now" %>
</button>
</form>
<%= if @error do %>
<p class="text-red-600 mt-2"><%= @error %></p>
<% end %>
Step 4: Create a PaymentIntent on the Server
def handle_event("checkout", _params, socket) do
{:ok, intent} = Stripe.PaymentIntent.create(%{
amount: 2000,
currency: "usd"
})
{:noreply,
socket
|> assign(:client_secret, intent.client_secret)
|> push_event("confirm_payment", %{client_secret: intent.client_secret})
|> assign(:submitting, true)}
end
Step 5: Handle the Result
def handle_event("payment_result", %{"error" => err}, socket) do
{:noreply, assign(socket, error: err["message"], submitting: false)}
end
def handle_event("payment_result", %{"paymentIntent" => _intent}, socket) do
{:noreply, assign(socket, :status, :success)}
end
Show success state:
<%= if @status == :success do %>
<div class="bg-green-100 text-green-800 p-4 mt-4">
✅ Payment confirmed!
</div>
<% end %>
Optional: Handle Stripe Webhooks
Use Stripe CLI to test locally:
stripe listen --forward-to localhost:4000/api/webhooks/stripe
In your router:
post "/api/webhooks/stripe", StripeWebhookController, :event
Process events like payment_intent.succeeded
and stream status back to LiveView via Phoenix.PubSub
.
Pro Tips
- ✅ Use
@submitting
assign to disable the form dynamically - ✅ Secure your API keys with environment variables
- ✅ Use
phx-hook
only for Stripe iframe; keep logic in Elixir - ✅ Show real-time status with
assign(:status, ...)
- ✅ Tailwind helps: use
bg-yellow-200
,opacity-50
,cursor-not-allowed
,animate-pulse
Use Cases
- 💳 One-time purchases
- 🧾 Subscriptions (via
Stripe.Subscription.create/1
) - 🎁 Donations with dynamic amounts
- 🛒 Multi-step checkouts with
live_step
Build responsive dashboards with:
live_stream(:invoices, ...)
assign(:subscription_status, ...)
And show:
- Live receipts
- Renewal dates
- Payment retries
- Upgrade/downgrade flows
TL;DR: Why This Works
- 👁 LiveView gives real-time feedback
- 🔐 Stripe handles security + compliance
- ☁️ Webhooks stream back status updates
- 🔧 Elixir keeps it reliable and testable
No SPA. No full reloads. No unnecessary JS complexity.
Learn More
📘 Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns
- Real-time payments, subscriptions, and status updates
- Webhook-driven UIs and server-led flows
- Performance tuning and resilience tips
Download it now — and master real-time, Stripe-integrated apps with confidence.