Installation
With Igniter (recommended for Phoenix)
Beta: The Igniter installer is new and under active testing. Report issues here.
If your project uses Igniter, one command adds the dependency and configures everything:
mix igniter.install tiger_stripe
This will:
- Add API key config to
config/dev.exs - Add runtime env var config to
config/runtime.exs - Add
Stripe.WebhookPlugto your endpoint (beforePlug.Parsers) - Scaffold a
StripeWebhookControllerwith event handler stubs - Add the webhook route to your router
Igniter shows a diff of all changes for your approval before writing anything. See the Igniter Installer guide for a detailed walkthrough, or the Webhooks guide for customizing the controller.
Manual
Add tiger_stripe to your dependencies in mix.exs:
def deps do
[
{:tiger_stripe, "~> 0.1.0"}
]
endRequires Elixir 1.19+ and OTP 27+.
Configuration
Add your Stripe credentials to your application config. The recommended
pattern is to use config/dev.exs for sandbox keys and config/runtime.exs
for production:
# config/dev.exs
import Config
config :tiger_stripe,
api_key: "sk_test_...",
webhook_secret: "whsec_test_..."# config/runtime.exs
import Config
if config_env() == :prod do
config :tiger_stripe,
api_key: System.fetch_env!("STRIPE_SECRET_KEY"),
webhook_secret: System.fetch_env!("STRIPE_WEBHOOK_SECRET")
endAll Config Options
The only required key is :api_key. Everything else has sensible defaults:
config :tiger_stripe,
# Required
api_key: "sk_test_...",
# Webhooks (required if using WebhookPlug)
webhook_secret: "whsec_...",
# Optional — all have defaults if omitted
api_version: "2026-01-28.clover", # pin API version (default: latest)
client_id: "ca_...", # OAuth client ID (Connect platforms)
max_retries: 3, # default: 2
open_timeout: 30_000, # connection timeout ms (default: 30,000)
read_timeout: 80_000, # read timeout ms (default: 80,000)
finch: MyApp.Finch # custom Finch pool (default: Stripe.Finch)| Key | Used By | Default | Description |
|---|---|---|---|
:api_key | Stripe.client/0,1,2 | required | Stripe secret key |
:webhook_secret | Stripe.WebhookPlug | — | Webhook signing secret |
:api_version | Stripe.client/0,1,2 | latest | Pin a specific API version |
:client_id | Stripe.client/0,1,2 | — | OAuth client ID (Connect) |
:max_retries | Stripe.client/0,1,2 | 2 | Max retry attempts |
:open_timeout | Stripe.client/0,1,2 | 30_000 | Connection timeout in ms |
:read_timeout | Stripe.client/0,1,2 | 80_000 | Read timeout in ms |
:finch | Stripe.client/0,1,2 | Stripe.Finch | Custom Finch pool name |
Creating a Client
Once configured, create a client with no arguments — it reads from your config automatically:
client = Stripe.client()Overriding Config
Pass options to override any config value for a specific client:
# Override just the connected account
client = Stripe.client(stripe_account: "acct_connected")
# Override retries and timeout
client = Stripe.client(max_retries: 5, read_timeout: 120_000)Explicit API Key
Pass a string to use a different API key (config defaults still apply for other options):
client = Stripe.client("sk_test_other_key")
client = Stripe.client("sk_test_other_key", max_retries: 5)Config Precedence
Options are resolved in this order (highest wins):
- Explicit arguments to
client/1orclient/2 - Application config (
config :tiger_stripe, ...) - Struct defaults (e.g.
max_retries: 2)
Clients are plain structs with no global state — safe for concurrent use with multiple API keys or connected accounts.
Making API Calls
Service modules map 1:1 to Stripe's API resources. Each method takes the client as the first argument:
# Create a customer
{:ok, customer} = Stripe.Services.CustomerService.create(client, %{
email: "jane@example.com"
})
# Retrieve a payment intent
{:ok, intent} = Stripe.Services.PaymentIntentService.retrieve(client, "pi_123")
# List charges
{:ok, charges} = Stripe.Services.ChargeService.list(client, %{"limit" => 10})Typed Responses
API responses are automatically deserialized into typed Elixir structs:
customer.id #=> "cus_abc123"
customer.email #=> "jane@example.com"
customer.__struct__ #=> Stripe.Resources.CustomerEvery resource struct has @type t definitions, so Dialyzer catches field
access errors at compile time.
Error Handling
All API errors return {:error, %Stripe.Error{}}:
case Stripe.Services.ChargeService.create(client, params) do
{:ok, charge} ->
charge
{:error, %Stripe.Error{type: :card_error} = err} ->
Logger.warning("Card declined: #{err.message}")
{:error, %Stripe.Error{type: :rate_limit_error}} ->
Process.sleep(1_000)
retry()
{:error, err} ->
Logger.error("Stripe error: #{err.message}")
endPer-Request Overrides
Options can be overridden per-request for Connect or multi-tenant scenarios:
Stripe.Services.ChargeService.list(client, %{},
stripe_account: "acct_connected",
api_version: "2025-12-18.acacia",
idempotency_key: "my-key-123"
)Pagination
V1 Lists
V1 list endpoints return %Stripe.ListObject{} with lazy auto-paging:
{:ok, page} = Stripe.Services.CustomerService.list(client, %{"limit" => 100})
page
|> Stripe.ListObject.auto_paging_stream(client)
|> Stream.filter(& &1.email)
|> Enum.take(50)Search Results
Search endpoints use token-based pagination via %Stripe.SearchResult{}:
{:ok, result} = Stripe.Services.ChargeService.search(client, %{
"query" => "amount>1000"
})
result
|> Stripe.SearchResult.auto_paging_stream(client)
|> Enum.to_list()V2 Lists
V2 endpoints return %Stripe.V2.ListObject{} with URL-based pagination:
{:ok, page} = Stripe.Services.V2.Core.AccountService.list(client)
page
|> Stripe.V2.ListObject.auto_paging_stream(client)
|> Enum.to_list()File Uploads
File params are automatically detected and encoded as multipart:
{:ok, file} = Stripe.Services.FileService.create(client, %{
"purpose" => "dispute_evidence",
"file" => %Stripe.Multipart.FilePath{path: "/path/to/receipt.pdf"}
})Streaming Responses
For large responses or server-sent events:
{:ok, chunks} =
Stripe.Client.stream_request(client, :get, "/v1/files/file_123/contents",
fn
{:data, chunk}, acc -> [chunk | acc]
_other, acc -> acc
end,
[]
)
body = chunks |> Enum.reverse() |> IO.iodata_to_binary()Raw Requests
Access the raw HTTP response without deserialization:
{:ok, resp} = Stripe.Client.raw_request(client, :get, "/v1/charges/ch_123")
resp.status #=> 200
resp.headers #=> [{"content-type", "application/json"}, ...]
resp.body #=> "{\"id\":\"ch_123\",...}"Retries
Failed requests (network errors, 409, 429, 500, 503) are automatically retried
with exponential backoff and jitter. The library respects Stripe's
stripe-should-retry response header.
# Via config
config :tiger_stripe, max_retries: 5
# Or per-client
client = Stripe.client(max_retries: 5)Idempotency keys are auto-generated for V2 POST/DELETE requests. For V1, pass them explicitly:
Stripe.Services.ChargeService.create(client, params,
idempotency_key: "order_123"
)Next Steps
- Webhooks — receive and verify Stripe events
- Connect & OAuth — multi-tenant and platform setups
- Testing — stub HTTP requests in your test suite
- Telemetry — observability and metrics