Current section
Files
Jump to
Current section
Files
README.md
# Ramp
[](https://hex.pm/packages/ramp)
[](https://hexdocs.pm/ramp)
A production-grade, dependency-light Elixir client for the
[Ramp Developer API](https://docs.ramp.com/developer-api/v1/overview/introduction) (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 `Stream`s, deferred-task polling,
and webhook signature verification all built in.
## Why Ramp
- **Genuinely standalone.** HTTP transport runs on Erlang/OTP's built-in
`:httpc`/`:ssl` and JSON on `Jason` -- the only two dependencies. No
`Req`/`Finch`/`Mint`/`Hackney` chain to pull in.
- **Idiomatic Elixir, not a Go port with the serial numbers filed off.**
List endpoints are lazy `Stream`s. Errors are one normalized exception
struct you pattern-match on. Token management is a supervised `GenServer`
with 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.
## Installation
```elixir
def deps do
[
{:ramp, "~> 1.0"}
]
end
```
## Quick start
```elixir
client =
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)
```
## Authentication
Three ways to authenticate, all via `Ramp.Client.new/1`:
```elixir
# 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.
## Pagination
Every `list/2` returns a lazily auto-paginating `Stream` -- pages are
fetched only as you consume them:
```elixir
# 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.
## Errors
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:
```elixir
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
end
```
`Ramp.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.
## Async writes
Card issuance, user invites, and limit creation are asynchronous on Ramp's
side. The corresponding `create/*` functions poll to completion by
default:
```elixir
{: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):
```elixir
{: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)
```
## Webhooks
Verify and decode inbound deliveries:
```elixir
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
end
```
Or use `Ramp.Webhooks.Handler` for declarative per-event-type dispatch:
```elixir
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`.
## Resource modules
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 |
## Telemetry
`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.
## Testing your integration
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.
## Development
```sh
mix deps.get
mix test
mix credo --strict
mix dialyzer
mix docs
```
## License
MIT. See [LICENSE](LICENSE).
This is an independent, community-maintained client and is not officially
affiliated with or endorsed by Ramp.