ramp-client
v1.0.0
Published
Production-grade TypeScript SDK for the Ramp Developer API v1 — OAuth 2.0, cursor pagination, deferred tasks, webhooks, and full resource coverage
Maintainers
Readme
ramp-client
Production-grade TypeScript SDK for the Ramp Developer API v1.
- Zero dependencies — native
fetch, Web Crypto API only - Full TypeScript — strict mode, exact optional properties, exhaustive types across all 17 resource namespaces
- OAuth 2.0 — client credentials + authorization code flows with automatic token refresh and mutex-safe concurrent requests
- Cursor pagination —
AsyncIterable<T>with.collect(),.take(n),.filter(),.find()combinators - Deferred tasks — async polling with exponential backoff for card/user/limit creation
- Webhook handler — HMAC-SHA256 verification, replay-attack protection, typed event routing
- Resilient HTTP — per-request retry with exponential backoff + jitter, 401 auto-refresh, configurable timeouts
- ESM + CJS — dual format with full declaration maps
Installation
npm install ramp-clientNode ≥ 18 required (uses native fetch and Web Crypto).
Quick Start
import { RampClient } from "ramp-client";
const ramp = new RampClient({
clientId: process.env.RAMP_CLIENT_ID!,
clientSecret: process.env.RAMP_CLIENT_SECRET!,
scopes: ["transactions:read", "cards:read", "users:read"],
});
// Fetch all SYNC_READY transactions across all pages
const transactions = await ramp.transactions
.list({ sync_status: "SYNC_READY" })
.collect();
// Get a single user
const user = await ramp.users.get("user-uuid");
// Create a department
const dept = await ramp.departments.create({ name: "Engineering" });Configuration
const ramp = new RampClient({
clientId: "...",
clientSecret: "...",
// Space-separated string or array — bound to the token at issuance
scopes: ["transactions:read", "cards:write", "users:read"],
// Override for sandbox testing (default: https://api.ramp.com)
baseUrl: "https://demo-api.ramp.com",
// Retry config (default: 3 retries, 500ms base delay, 30s timeout)
maxRetries: 3,
retryDelayMs: 500,
timeoutMs: 30_000,
// Optional structured logger
logger: {
debug: (msg, meta) => console.debug(msg, meta),
info: (msg, meta) => console.info(msg, meta),
warn: (msg, meta) => console.warn(msg, meta),
error: (msg, meta) => console.error(msg, meta),
},
});Authentication
Client Credentials (default)
Token is fetched automatically on the first API call. Subsequent calls reuse the cached token; it is refreshed proactively 60 seconds before expiry. Concurrent calls are safe — only one token fetch fires at a time (mutex via shared Promise).
Authorization Code (partner / multi-tenant)
// Redirect the user to Ramp's authorization page, then exchange the code:
const tokenResponse = await ramp.exchangeAuthCode({
code: req.query.code,
redirectUri: "https://yourapp.com/oauth/callback",
});
// From this point all calls use the exchanged token.Pre-obtained Token
// Useful for CLI tools, testing, or when you manage token storage yourself:
ramp.setAccessToken("ramp_tok_...", 864_000); // expiresInSeconds optionalResources
Users
// List — returns AsyncIterable PageIterator
for await (const user of ramp.users.list({ department_id: "dept-uuid" })) {
console.log(user.email, user.status);
}
// Collect all into array
const all = await ramp.users.list().collect();
// Get one
const user = await ramp.users.get("user-uuid");
// Create (deferred — user gets an invite email)
// poll=false returns DeferredTaskRef immediately (default)
const taskRef = await ramp.users.create({
email: "[email protected]",
first_name: "Alice",
last_name: "Smith",
role: "CARDHOLDER",
department_id: "dept-uuid",
});
// poll=true waits for the task to reach SUCCESS or ERROR
const newUser = await ramp.users.create({ ... }, true);
// Update
await ramp.users.update("user-uuid", { role: "ADMIN" });
// Deactivate / reactivate
await ramp.users.deactivate("user-uuid");
await ramp.users.reactivate("user-uuid");Important: New users are in
INVITE_PENDINGstatus until they accept the email invitation. Do not attempt card issuance until the user isUSER_ACTIVE.
Cards
// List by user and status
const cards = await ramp.cards
.list({ user_id: "user-uuid", status: "ACTIVE" })
.collect();
// Issue a new virtual card (deferred + idempotency key required)
const taskRef = await ramp.cards.create({
display_name: "AWS Infra",
user_id: "user-uuid",
spend_limit_id: "limit-uuid", // optional
idempotency_key: crypto.randomUUID(),
});
// Suspend / unsuspend / terminate
await ramp.cards.suspend("card-uuid");
await ramp.cards.unsuspend("card-uuid");
await ramp.cards.terminate("card-uuid");Transactions
// Full filter surface
const txns = await ramp.transactions
.list({
sync_status: "SYNC_READY",
from_date: "2024-01-01T00:00:00Z",
to_date: "2024-01-31T23:59:59Z",
state: "CLEARED",
entity_id: "entity-uuid", // multi-entity support
})
.collect();
// Update memo and accounting fields
await ramp.transactions.update("txn-uuid", {
memo: "Q1 cloud infra",
accounting_fields: [
{ field_id: "dept-field-uuid", option_id: "engineering-uuid" },
],
});Limits (Spend Controls)
// Create a limit (deferred, requires idempotency key)
const limit = await ramp.limits.create(
{
display_name: "Q2 Marketing Budget",
user_id: "user-uuid",
balance: { amount: 500_000, currency_code: "USD" }, // $5,000.00
idempotency_key: crypto.randomUUID(),
},
true,
); // poll=true → returns Limit object
// Update
await ramp.limits.update("limit-uuid", {
balance: { amount: 1_000_000, currency_code: "USD" },
});
// Terminate (permanent, deferred)
await ramp.limits.terminate("limit-uuid", true);Bills (Accounts Payable)
// Create a bill (auto-approved when created via API)
const bill = await ramp.bills.create({
vendor_id: "vendor-uuid",
amount: { amount: 250_000, currency_code: "USD" },
invoice_number: "INV-2024-001",
due_at: "2024-02-28T00:00:00Z",
payment_method: "ACH",
});
// List by vendor and status
const pendingBills = await ramp.bills
.list({
vendor_id: "vendor-uuid",
status: "APPROVED",
})
.collect();
// Void a bill
await ramp.bills.void("bill-uuid");Accounting / ERP Sync
The core ERP integration loop:
// 1. Fetch all objects ready to sync
const readyTxns = await ramp.transactions
.list({ sync_status: "SYNC_READY" })
.collect();
// 2. Process in your ERP...
// 3. Report results back to Ramp
await ramp.accounting.postSyncStatus({
idempotency_key: crypto.randomUUID(),
syncs: readyTxns.map((t) => ({
object_id: t.id,
object_type: "TRANSACTION",
sync_status: "SUCCESS",
})),
});// Manage GL accounts
await ramp.accounting.createGLAccounts([
{ name: "Travel Expenses", code: "6100", remote_id: "erp-gl-6100" },
{ name: "Software & SaaS", code: "6200", remote_id: "erp-gl-6200" },
]);
const glAccounts = await ramp.accounting
.listGLAccounts({ is_active: true })
.collect();
// Manage custom fields and options
await ramp.accounting.createField({
name: "Department",
input_type: "SELECT",
is_required: true,
});
await ramp.accounting.createFieldOptions("field-uuid", [
{ name: "Engineering", code: "ENG", remote_id: "erp-dept-eng" },
{ name: "Marketing", code: "MKT", remote_id: "erp-dept-mkt" },
]);Departments & Locations
const dept = await ramp.departments.create({ name: "Engineering" });
await ramp.departments.update(dept.id, { name: "Platform Engineering" });
await ramp.departments.delete(dept.id);
const loc = await ramp.locations.create({ name: "San Francisco HQ" });Webhooks
// Register a webhook endpoint
const webhook = await ramp.webhooks.create({
url: "https://yourapp.com/webhooks/ramp",
event_types: [
"transaction.created",
"transaction.updated",
"card.activated",
"card.suspended",
"user.created",
"bill.created",
],
});
// Store webhook.secret_token securely for HMAC verificationPagination
All list methods return a PageIterator<T> — an AsyncIterable<T> with helper methods:
const iter = ramp.transactions.list({ sync_status: "SYNC_READY" });
// Async iteration (memory-efficient for large datasets)
for await (const txn of iter) {
await processTransaction(txn);
}
// Collect all into array
const all = await iter.collect();
// Take at most N items (stops fetching early)
const first10 = await iter.take(10);
// Filter across pages
const cleared = await iter.filter((t) => t.state === "CLEARED");
// Find first matching item
const target = await iter.find((t) => t.merchant_name === "AWS");
// Fetch a single page explicitly
const page1 = await iter.page();
const page2 = await iter.page(page1.page.next ?? undefined);Deferred Tasks
Card issuance, user creation, and limit creation are asynchronous — the API returns a task reference immediately.
// Option 1: fire-and-forget, poll manually
const taskRef = await ramp.cards.create({
display_name: "Travel Card",
user_id: "user-uuid",
idempotency_key: crypto.randomUUID(),
}); // returns { id: "task-uuid" }
// Poll the task yourself
import { DeferredPoller } from "ramp-client";
const poller = new DeferredPoller(httpClient);
const task = await poller.poll(taskRef, {
intervalMs: 500,
maxIntervalMs: 5_000,
timeoutMs: 60_000,
});
// Option 2: poll automatically (poll=true)
const card = await ramp.cards.create(
{
display_name: "Travel Card",
user_id: "user-uuid",
idempotency_key: crypto.randomUUID(),
},
true,
); // returns Card when readyWebhook Verification
import { WebhookHandler } from "ramp-client";
// or: import { WebhookHandler } from "ramp-client/webhooks";
const handler = new WebhookHandler({
secret: process.env.RAMP_WEBHOOK_SECRET!,
toleranceMs: 300_000, // 5-minute replay window (default)
});
// Register typed handlers
handler
.on("transaction.created", async (event) => {
const txn = event.data as { id: string; amount: number };
await syncToERP(txn);
})
.on("card.suspended", (event) => {
console.log("Card suspended:", event.data);
})
.on("*", (event) => {
// Wildcard — fires for every event type
console.log("Ramp event:", event.type, event.id);
});
// Express.js / Hono / any framework:
app.post("/webhooks/ramp", async (req, res) => {
try {
await handler.handle(req.rawBody, req.headers as Record<string, string>);
res.sendStatus(200);
} catch (err) {
if (err instanceof RampError && err.type === "authentication_error") {
res.sendStatus(401);
} else {
res.sendStatus(500);
}
}
});Error Handling
All API errors throw a RampError with a discriminated type field:
import { RampError } from "ramp-client";
try {
await ramp.users.get("nonexistent-uuid");
} catch (err) {
if (err instanceof RampError) {
switch (err.type) {
case "not_found":
console.error("User does not exist");
break;
case "rate_limit_error":
console.error(`Rate limited. Retry after ${err.retryAfterMs}ms`);
break;
case "authentication_error":
console.error("Invalid credentials — check client_id/secret");
break;
case "authorization_error":
console.error("Missing scope — check OAuth scopes");
break;
case "validation_error":
console.error("Bad request:", err.body);
break;
case "server_error":
case "network_error":
if (err.isRetryable()) {
// HttpClient already retried maxRetries times
}
break;
}
// Every error includes the x-trace-id for Ramp support
console.log("Trace ID:", err.traceId);
}
}| Type | HTTP Status | Retryable |
| ---------------------- | ----------- | ------------------ |
| authentication_error | 401 | No |
| authorization_error | 403 | No |
| not_found | 404 | No |
| validation_error | 400 | No |
| rate_limit_error | 429 | Yes (auto-retried) |
| server_error | 5xx | Yes (auto-retried) |
| network_error | — | Yes (auto-retried) |
| timeout_error | — | No |
Sandbox
const ramp = new RampClient({
clientId: process.env.RAMP_SANDBOX_CLIENT_ID!,
clientSecret: process.env.RAMP_SANDBOX_CLIENT_SECRET!,
baseUrl: "https://demo-api.ramp.com",
});Sandbox access requires a separate application — contact your Ramp account manager. Sandbox credentials are not interchangeable with production.
API Reference
RampClient
| Resource | Namespace |
| -------------- | --------------------- |
| Users | ramp.users |
| Cards | ramp.cards |
| Transactions | ramp.transactions |
| Limits | ramp.limits |
| Bills | ramp.bills |
| Reimbursements | ramp.reimbursements |
| Accounting | ramp.accounting |
| Webhooks | ramp.webhooks |
| Departments | ramp.departments |
| Locations | ramp.locations |
| Vendors | ramp.vendors |
| Merchants | ramp.merchants |
| Spend Programs | ramp.spendPrograms |
| Statements | ramp.statements |
| Business | ramp.business |
| Audit Logs | ramp.auditLogs |
| Cashbacks | ramp.cashbacks |
License
MIT
