Ramp

Hex.pmDocs

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.

Why Ramp

Installation

def deps do
[
{:ramp, "~> 1.0"}
]
end

Quick start

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:

# 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:

# 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:

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:

{: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)

Webhooks

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
end

Or 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.

Resource modules

Every resource follows the same shape: list/2 (a Stream), get/2, and whatever writes it supports.

ModuleRamp resource
Ramp.AccountingERP connections, GL accounts, custom fields, sync
Ramp.AuditLogsAudit event log
Ramp.BillsAccounts payable
Ramp.BusinessThe business account (singleton)
Ramp.CardsCard issuance and lifecycle
Ramp.CashbacksCashback rewards
Ramp.DepartmentsOrg departments
Ramp.EntitiesLegal entities
Ramp.LimitsSpend controls
Ramp.LocationsOffice locations
Ramp.MerchantsCard-network merchants
Ramp.ReimbursementsOut-of-pocket expense reimbursements
Ramp.SpendProgramsSpend program templates
Ramp.StatementsBilling statements
Ramp.TransactionsCard transactions
Ramp.UsersPlatform users
Ramp.VendorsAP vendors
Ramp.WebhookSubscriptionsWebhook 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

mix deps.get
mix test
mix credo --strict
mix dialyzer
mix docs

License

MIT. See LICENSE.

This is an independent, community-maintained client and is not officially affiliated with or endorsed by Ramp.