LatticeStripe is a production-grade Elixir SDK for the Stripe API. This guide walks you through installation, setup, and your first API call — from zero to a working PaymentIntent in just a few minutes.
Installation
Add lattice_stripe to your dependencies in mix.exs:
defp deps do
[
{:lattice_stripe, "~> 0.1"},
{:finch, "~> 0.21"}
]
endThen fetch your dependencies:
$ mix deps.get
Note: Finch is listed separately here to make it explicit. LatticeStripe declares Finch as a dependency, but listing it in your app's
mix.exslets you configure the version you want.
Setting Up Finch
LatticeStripe uses Finch as its HTTP client. Finch is a connection-pooling HTTP library built on Mint — the modern standard for HTTP in Elixir.
You need to start a Finch pool in your application's supervision tree. Add it to
lib/my_app/application.ex:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Start the Finch HTTP client for LatticeStripe
{Finch, name: MyApp.Finch},
# ... your other children
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endThe name: option is an atom that identifies this pool. You'll pass the same atom when
creating a LatticeStripe client.
Non-OTP scripts: If you're writing a one-off script (not a full OTP application), start Finch manually before making API calls:
{:ok, _} = Finch.start_link(name: MyApp.Finch)
Creating a Client
LatticeStripe is configured through a plain struct — no global state, no config files. Create a client with your Stripe API key and your Finch pool name:
client = LatticeStripe.Client.new!(
api_key: "sk_test_YOUR_STRIPE_TEST_KEY",
finch: MyApp.Finch
)Both api_key and finch are required. Everything else has sensible defaults.
Where to get your API keys: Log in to the Stripe Dashboard.
Use sk_test_... keys in development — they don't charge real cards.
Storing the Client
The client is a plain %LatticeStripe.Client{} struct. There's no process behind it —
you can store it anywhere that makes sense for your app:
# As a module attribute (simple, read-only)
defmodule MyApp.Stripe do
@client LatticeStripe.Client.new!(
api_key: Application.fetch_env!(:my_app, :stripe_api_key),
finch: MyApp.Finch
)
def client, do: @client
endOr create it at runtime and pass it through your function calls. The struct is safe to share across processes.
Your First API Call
With a client in hand, you can make Stripe API calls. Let's create a PaymentIntent — the core object for accepting payments in Stripe's modern payment flow:
{:ok, intent} = LatticeStripe.PaymentIntent.create(client, %{
"amount" => 2000,
"currency" => "usd"
})
IO.puts("Created PaymentIntent: #{intent.id}")
IO.puts("Amount: $#{intent.amount / 100}")
IO.puts("Status: #{intent.status}")Run this and you'll see output like:
Created PaymentIntent: pi_3OzqKZ2eZvKYlo2C1FRzQc8s
Amount: $20.0
Status: requires_payment_methodA few things to note:
- Amount is in cents.
2000means $20.00 USD. Always use the smallest currency unit. - The response is a struct.
intentis a%LatticeStripe.PaymentIntent{}— all fields are accessible as atoms. - Test mode is safe. Using
sk_test_...keys means no real charges happen.
Handling Errors
All LatticeStripe functions return {:ok, result} on success or {:error, %LatticeStripe.Error{}} on failure. Pattern match on the result to handle errors gracefully:
case LatticeStripe.PaymentIntent.create(client, %{
"amount" => 2000,
"currency" => "usd"
}) do
{:ok, intent} ->
IO.puts("Created PaymentIntent: #{intent.id}")
{:error, %LatticeStripe.Error{type: :card_error} = err} ->
IO.puts("Card declined: #{err.message}")
IO.puts("Decline code: #{err.decline_code}")
{:error, %LatticeStripe.Error{type: :rate_limit_error}} ->
IO.puts("Too many requests — back off and retry")
{:error, %LatticeStripe.Error{type: :authentication_error}} ->
IO.puts("Invalid API key — check your credentials")
{:error, %LatticeStripe.Error{} = err} ->
IO.puts("Stripe error: #{err.message} (#{err.type})")
endThe LatticeStripe.Error struct contains:
type— atom like:card_error,:invalid_request_error,:authentication_error,:rate_limit_error,:api_errormessage— human-readable descriptioncode— Stripe error code (e.g.,"card_declined")param— the invalid parameter, for validation errorsstatus— the HTTP status coderequest_id— Stripe's request ID, useful for support tickets
Bang Variants
If you'd rather raise on error than pattern match, every function has a bang variant:
# Raises LatticeStripe.Error if the call fails
intent = LatticeStripe.PaymentIntent.create!(client, %{
"amount" => 2000,
"currency" => "usd"
})Use the ! variants in scripts and places where you want to fail loudly. Use the
non-bang variants in production code where you need to handle errors gracefully.
Next Steps
Now that you've made your first API call, explore the rest of LatticeStripe:
- Client Configuration — All client options, per-request overrides, multiple clients, Stripe Connect.
- Payments — Full payment lifecycle: customers, PaymentIntents, confirmation, capture, refunds, idempotency.
- Checkout — Stripe's hosted payment page. Payment, subscription, and setup modes.
- Webhooks — Signature verification, Phoenix Plug setup, event handling.
Common Pitfalls
Finch must be started before making API calls.
If you see (Finch.Error) no pool found errors, Finch isn't in your supervision tree or
hasn't started yet. Make sure {Finch, name: MyApp.Finch} is in your children list in
application.ex.
Amount is in cents, not dollars.
2000 means $20.00 USD, not $2,000. This is a very common mistake that produces confusing
results. The Stripe API always uses the smallest unit of the currency (cents for USD,
pence for GBP, etc.).
Use test mode keys (sk_test_...) in development.
Test keys start with sk_test_. Live keys start with sk_live_. Never use live keys in
development or CI — test mode keys can't charge real cards, so mistakes are harmless.
Client is a struct, not a process. You don't need to start a GenServer or add LatticeStripe to your supervision tree (beyond Finch). Just create the struct and pass it around. It's safe to share across processes.
Validation errors raise, not return {:error, ...}.
If you pass invalid options to Client.new!, it raises NimbleOptions.ValidationError
immediately. This catches typos and misconfiguration at startup, not at request time.