TinyElixirStripe
Warning
This library has been retired and renamed to PinStripe! Don't use this version. All active development will take place on PinStripe.
Stripe doesn't provide an official Elixir SDK, and maintaining a full-featured SDK has proven to be a challenge. As I see it, this is because the Elixir community is pretty senior-driven. People have no problem rolling their own integration with the incredible Req library.
This library is an attempt to wrap those community learnings in an easy-to-use package modeled after patterns set forth in Dashbit's SDKs with Req: Stripe article by Wojtek Mach.
My hope is that this should suffice for 95% of all apps that need to integrate with Stripe, and that the remaining 5% of use cases have a built-in escape hatch with Req.
Features
- Simple API Client built on Req with automatic ID prefix recognition
- Webhook Handler DSL using Spark for clean, declarative webhook handling
- Automatic Signature Verification for webhook security
- Code Generators powered by Igniter for zero-config setup
- Sync with Stripe to keep your local handlers in sync with your Stripe dashboard
Installation
Using Igniter (Recommended)
The fastest way to install is using the Igniter installer:
mix igniter.install tiny_elixir_stripeThis will:
-
Add the dependency to your
mix.exs -
Replace
Plug.ParserswithTinyElixirStripe.ParsersWithRawBodyin your Phoenix endpoint (required for webhook signature verification) -
Create a
StripeWebhookHandlersmodule for defining event handlers -
Generate a
StripeWebhookControllerin your Phoenix app -
Add the webhook route to your router (default:
/webhooks/stripe) -
Configure
.formatter.exsfor DSL formatting support
Then configure your Stripe credentials in config/runtime.exs:
config :tiny_elixir_stripe,
stripe_api_key: System.get_env("YOUR_STRIPE_KEY_ENV_VAR"),
stripe_webhook_secret: System.get_env("YOUR_WEBHOOK_SECRET_ENV_VAR")Manual Installation
If you prefer not to use Igniter, add to your mix.exs:
def deps do
[
{:tiny_elixir_stripe, "~> 0.1"}
]
endThen follow the Manual Setup instructions below.
Changing the Webhook Path
The default webhook path is /webhooks/stripe. If you need to change it later:
mix tiny_elixir_stripe.set_webhook_path /new/webhook/pathManual Installation (without Igniter)
If you prefer not to use Igniter, you'll need to manually:
- Replace Plug.Parsers in your endpoint (
lib/my_app_web/endpoint.ex):
# Replace this:
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason
# With this:
plug TinyElixirStripe.ParsersWithRawBody,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason- Create a webhook handler module (
lib/my_app/stripe_webhook_handlers.ex):
defmodule MyApp.StripeWebhookHandlers do
use TinyElixirStripe.WebhookHandler
# Define your handlers here (see examples below)
end- Create a webhook controller (
lib/my_app_web/stripe_webhook_controller.ex):
defmodule MyAppWeb.StripeWebhookController do
use TinyElixirStripe.WebhookController,
handler: MyApp.StripeWebhookHandlers
end- Add the route to your router (
lib/my_app_web/router.ex):
scope "/webhooks" do
post "/stripe", MyAppWeb.StripeWebhookController, :create
end- Add formatter config to
.formatter.exs:
[
import_deps: [:tiny_elixir_stripe],
# ... rest of config
]Handling Stripe Webhooks
TinyElixirStripe provides a clean DSL for handling webhook events. When Stripe sends a webhook to your endpoint, the controller automatically:
- Verifies the webhook signature using your signing secret
- Parses the event
- Dispatches it to the appropriate handler
Function Handlers
Define inline handlers for simple event processing:
defmodule MyApp.StripeWebhookHandlers do
use TinyElixirStripe.WebhookHandler
handle "customer.created", fn event ->
customer_id = event["data"]["object"]["id"]
email = event["data"]["object"]["email"]
# Your business logic here
MyApp.Customers.create_from_stripe(customer_id, email)
:ok
end
handle "customer.updated", fn event ->
# Handle customer updates
:ok
end
handle "invoice.payment_succeeded", fn event ->
# Handle successful payments
:ok
end
endModule Handlers
For more complex event processing, use separate modules:
defmodule MyApp.StripeWebhookHandlers do
use TinyElixirStripe.WebhookHandler
handle "customer.subscription.created", MyApp.StripeWebhookHandlers.SubscriptionCreated
handle "customer.subscription.updated", MyApp.StripeWebhookHandlers.SubscriptionUpdated
handle "customer.subscription.deleted", MyApp.StripeWebhookHandlers.SubscriptionDeleted
enddefmodule MyApp.StripeWebhookHandlers.SubscriptionCreated do
@moduledoc """
Handles subscription creation events.
"""
def handle_event(event) do
subscription = event["data"]["object"]
customer_id = subscription["customer"]
# Complex business logic
with {:ok, user} <- MyApp.Users.find_by_stripe_customer(customer_id),
{:ok, _subscription} <- MyApp.Subscriptions.create(user, subscription) do
:ok
else
{:error, reason} ->
{:error, reason}
end
end
endGenerating Handlers
Use the generator to quickly scaffold handlers:
# Generates a function handler
mix tiny_elixir_stripe.gen.handler customer.created
# Generates a module handler
mix tiny_elixir_stripe.gen.handler customer.subscription.created --handler-type module
# Generates with custom module name
mix tiny_elixir_stripe.gen.handler charge.succeeded --handler-type module --module MyApp.Payments.ChargeHandlerThe generator will:
- Validate the event name against supported Stripe events
-
Create the handler in your
WebhookHandlermodule - Generate a separate module file for module handlers
Syncing with Stripe
Keep your local handlers in sync with your Stripe webhook configuration:
mix tiny_elixir_stripe.sync_webhook_handlersThis task will:
- Fetch all webhook endpoints from your Stripe account
- Extract all enabled events
- Compare them with your existing handlers
- Generate stub handlers for any missing events
Options:
--handler-type function|module|ask- Choose handler type for all missing events--skip-confirmationor-y- Skip prompts and generate all handlers--api-keyor-k- Specify Stripe API key (otherwise uses config or prompts)
Example output:
Fetching webhook endpoints from Stripe...
Found 1 webhook endpoint(s):
• https://myapp.com/webhooks/stripe (5 events)
Collecting all enabled events...
Events configured in Stripe:
✓ customer.created (handler exists)
✗ customer.updated (missing)
✗ invoice.payment_succeeded (missing)
✓ subscription.created (handler exists)
✗ subscription.deleted (missing)
Found 3 missing handler(s) out of 5 total Stripe event(s).
Generate handlers for missing events? (y/n) y
What type of handlers would you like to generate?
1. function
2. module
3. ask
> 1
Generating handlers...
• customer.updated (function handler)
• invoice.payment_succeeded (function handler)
• subscription.deleted (function handler)
✓ Done! Generated 3 new handler(s).Calling the Stripe API
The TinyElixirStripe.Client module provides a simple CRUD interface for interacting with the Stripe API, built on Req.
Basic Usage
alias TinyElixirStripe.Client
# Fetch a customer by ID
{:ok, response} = Client.read("cus_123")
customer = response.body
# List customers with pagination
{:ok, response} = Client.read(:customers, limit: 10, starting_after: "cus_123")
customers = response.body["data"]
# Create a customer
{:ok, response} = Client.create(:customers, %{
email: "customer@example.com",
name: "Jane Doe",
metadata: %{user_id: "12345"}
})
# Update a customer
{:ok, response} = Client.update("cus_123", %{
name: "Jane Smith",
metadata: %{premium: true}
})
# Delete a customer
{:ok, response} = Client.delete("cus_123")Automatic ID Recognition
The client automatically recognizes Stripe ID prefixes:
Client.read("cus_123") # => /customers/cus_123
Client.read("sub_456") # => /subscriptions/sub_456
Client.read("price_789") # => /prices/price_789
Client.read("product_abc") # => /products/product_abc
Client.read("inv_xyz") # => /invoices/inv_xyz
Client.read("evt_123") # => /events/evt_123
Client.read("cs_test_abc") # => /checkout/sessions/cs_test_abcSupported Entity Types
Use atoms for entity types when creating or listing:
Client.create(:customers, %{email: "test@example.com"})
Client.create(:subscriptions, %{customer: "cus_123", items: [%{price: "price_abc"}]})
Client.create(:products, %{name: "Premium Plan"})
Client.create(:prices, %{product: "prod_123", unit_amount: 1000, currency: "usd"})
Client.create(:checkout_sessions, %{mode: "payment", line_items: [...]})
Client.read(:customers, limit: 100)
Client.read(:subscriptions, customer: "cus_123")
Client.read(:invoices, status: "paid")Bang Functions
Use ! versions to raise on errors:
# Raises RuntimeError on failure
response = Client.read!("cus_123")
customer = Client.create!(:customers, %{email: "test@example.com"})Advanced Usage with Req
Since the client is built on Req, you can access the full Req API:
# Direct Req request with custom options
{:ok, response} = Client.request("/charges/ch_123", retry: :transient)
# Or build a custom client
client = Client.new(receive_timeout: 30_000)
{:ok, response} = Req.get(client, url: "/customers/cus_123")Testing
Configure Req.Test for testing:
# config/test.exs
config :tiny_elixir_stripe,
req_options: [plug: {Req.Test, TinyElixirStripe}]In your tests:
test "creates a customer" do
Req.Test.stub(TinyElixirStripe, fn conn ->
Req.Test.json(conn, %{
id: "cus_test_123",
email: "test@example.com",
object: "customer"
})
end)
{:ok, response} = Client.create(:customers, %{email: "test@example.com"})
assert response.body["id"] == "cus_test_123"
endConfiguration
# config/config.exs
config :tiny_elixir_stripe,
stripe_api_key: System.get_env("STRIPE_SECRET_KEY"),
stripe_webhook_secret: System.get_env("STRIPE_WEBHOOK_SECRET")
# config/test.exs
config :tiny_elixir_stripe,
req_options: [plug: {Req.Test, TinyElixirStripe}]Special Thanks
- Stripity Stripe
- Wojtek Mach
- Dashbit
- Zach Daniel and the Ash Team
- All contributors to this discussion