A production-grade, dependency-light Elixir client for the Ramp Developer API (v1).
Ramp covers the full Ramp API surface -- cards, users, transactions,
spend limits, bills, vendors, merchants, entities, departments, locations,
statements, cashbacks, spend programs, reimbursements, audit logs,
accounting sync, and webhooks -- with OAuth2 token management, automatic
retries, cursor-based pagination as lazy Streams, deferred-task polling,
and webhook signature verification all built in.
- Genuinely standalone. HTTP transport runs on Erlang/OTP's built-in
:httpc/:ssland JSON onJason-- the only two dependencies. NoReq/Finch/Mint/Hackneychain to pull in. - Idiomatic Elixir, not a Go port with the serial numbers filed off.
List endpoints are lazy
Streams. Errors are one normalized exception struct you pattern-match on. Token management is a supervisedGenServerwith correct single-flight refresh semantics for free. - Handles the annoying parts for you: exponential backoff with jitter,
honoring
Retry-After, one-shot 401 token refresh, idempotency keys, deferred-task polling, and constant-time webhook signature verification with replay protection.
def deps do
[
{:ramp, "~> 1.0"}
]
endclient =
Ramp.Client.new(
client_id: System.fetch_env!("RAMP_CLIENT_ID"),
client_secret: System.fetch_env!("RAMP_CLIENT_SECRET"),
scopes: ~w(transactions:read cards:read cards:write users:read)
)
{:ok, card} = Ramp.Cards.get(client, "crd_1a2b3c")
client
|> Ramp.Transactions.list(state: "CLEARED")
|> Stream.take(50)
|> Enum.each(&IO.inspect/1)Three ways to authenticate, all via Ramp.Client.new/1:
# 1. client_credentials (recommended) -- starts and manages its own
# unsupervised TokenManager, linked to the calling process.
Ramp.Client.new(
client_id: "...",
client_secret: "...",
scopes: ~w(transactions:read cards:read)
)
# 2. A pre-obtained access token (e.g. you manage refresh yourself).
Ramp.Client.new(access_token: "ramp_at_...", expires_in: 600)
# 3. An externally supervised TokenManager -- the recommended setup for
# long-running production apps. Add it to your own supervision tree:
children = [
{Ramp.TokenManager,
name: MyApp.RampTokenManager,
client_id: System.fetch_env!("RAMP_CLIENT_ID"),
client_secret: System.fetch_env!("RAMP_CLIENT_SECRET"),
scopes: ~w(transactions:read cards:read)}
]
Supervisor.start_link(children, strategy: :one_for_one)
client = Ramp.Client.new(token_manager: MyApp.RampTokenManager)Use sandbox: true (or base_url: for a custom endpoint) to hit Ramp's
sandbox environment instead of production.
Every list/2 returns a lazily auto-paginating Stream -- pages are
fetched only as you consume them:
# Fetches only as many pages as needed.
client
|> Ramp.Transactions.list(state: "CLEARED")
|> Enum.find(&(&1.amount > 10_000))
# Fully materializes -- fetches every page.
all_cards = client |> Ramp.Cards.list() |> Enum.to_list()Prefer manual control over pagination? Every resource also exposes
list_page/2, returning one Ramp.Page{data: [...], next_cursor: ...}
at a time.
Every operation returns {:ok, result} or {:error, %Ramp.Error{}}.
Ramp.Error normalizes every failure mode -- HTTP error responses,
network failures, timeouts, deferred-task failures -- into one struct:
case Ramp.Cards.get(client, "nonexistent") do
{:ok, card} -> card
{:error, error} ->
cond do
Ramp.Error.not_found?(error) -> :not_found
Ramp.Error.rate_limited?(error) -> :try_again_later
true -> raise error
end
endRamp.HTTP already retries retryable errors (429/5xx/network
failures) automatically with exponential backoff + jitter, honoring
Retry-After -- you're only ever handed an error after retries are
exhausted.
Card issuance, user invites, and limit creation are asynchronous on Ramp's
side. The corresponding create/* functions poll to completion by
default:
{:ok, card} = Ramp.Cards.create(client, "usr_123", "Marketing Team Card")Pass poll: false to instead get a Ramp.DeferredTaskRef back
immediately and poll it yourself (e.g. from another process):
{:ok, task_ref} = Ramp.Cards.create(client, "usr_123", "Ops Card", poll: false)
{:ok, card_json} = Ramp.Poller.poll(client, task_ref.id, timeout_ms: 30_000)Verify and decode inbound deliveries:
def webhook(conn, _params) do
{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
signature = conn |> Plug.Conn.get_req_header("x-webhook-signature") |> List.first()
timestamp = conn |> Plug.Conn.get_req_header("x-webhook-timestamp") |> List.first()
case Ramp.Webhooks.construct_event(raw_body, signature, timestamp, @webhook_secret) do
{:ok, event} ->
MyApp.WebhookProcessor.handle(event)
send_resp(conn, 200, "ok")
{:error, %Ramp.Error{}} ->
send_resp(conn, 400, "invalid signature")
end
endOr use Ramp.Webhooks.Handler for declarative per-event-type dispatch:
handler =
Ramp.Webhooks.Handler.new(webhook_secret: @webhook_secret)
|> Ramp.Webhooks.Handler.on("card.created", &MyApp.Cards.on_created/1)
|> Ramp.Webhooks.Handler.on("card.terminated", &MyApp.Cards.on_terminated/1)
Ramp.Webhooks.Handler.handle(handler, raw_body, signature, timestamp)Manage which events get delivered where with Ramp.WebhookSubscriptions.
Every resource follows the same shape: list/2 (a Stream), get/2, and
whatever writes it supports.
| Module | Ramp resource |
|---|---|
Ramp.Accounting |
ERP connections, GL accounts, custom fields, sync |
Ramp.AuditLogs |
Audit event log |
Ramp.Bills |
Accounts payable |
Ramp.Business |
The business account (singleton) |
Ramp.Cards |
Card issuance and lifecycle |
Ramp.Cashbacks |
Cashback rewards |
Ramp.Departments |
Org departments |
Ramp.Entities |
Legal entities |
Ramp.Limits |
Spend controls |
Ramp.Locations |
Office locations |
Ramp.Merchants |
Card-network merchants |
Ramp.Reimbursements |
Out-of-pocket expense reimbursements |
Ramp.SpendPrograms |
Spend program templates |
Ramp.Statements |
Billing statements |
Ramp.Transactions |
Card transactions |
Ramp.Users |
Platform users |
Ramp.Vendors |
AP vendors |
Ramp.WebhookSubscriptions |
Webhook endpoint registrations |
Ramp.HTTP emits :telemetry.span/3 events under
[:ramp, :http, :request] (:start, :stop, :exception) with
%{method:, path:, attempt:} metadata -- attach a handler for logging,
metrics, or tracing.
Point a client at Ramp's sandbox with sandbox: true. The test suite in
this repo also includes test/support/mock_server.ex, a small
dependency-free :gen_tcp-based HTTP mock server, if you'd like a
lightweight pattern for testing your own code that calls into Ramp.
mix deps.get
mix test
mix credo --strict
mix dialyzer
mix docsMIT. See LICENSE.
This is an independent, community-maintained client and is not officially affiliated with or endorsed by Ramp.