Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ramp

Hex.pm Docs

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

  • 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 Streams. 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

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.

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

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.

About

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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages