ramp

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 25 Imported by: 0

README

ramp-go

Production-grade Go client for the Ramp Developer API v1.

  • Zero external dependencies — pure stdlib only
  • Domain-Driven Design — one package per resource domain, clean separation of concerns
  • Generic cursor pagination*pagination.Iterator[T] with .Collect(), .Take(n) across all list endpoints
  • OAuth 2.0 — client credentials + authorization code, mutex single-flight token fetch, proactive refresh
  • Deferred task polling — typed poller.Poll[T] with exponential backoff for card/user/limit creation
  • Webhook handler — HMAC-SHA256 verification, replay-attack protection, typed event routing, http.Handler compatible
  • Resilient HTTP — per-request retry with full-jitter exponential backoff, 401 auto-refresh, configurable timeouts
  • Structured errors*ramp.Error with discriminated Type, TraceID, RetryAfter, IsRetryable()
  • log/slog native — structured logging throughout, bring your own logger

Installation

go get github.com/iamkanishka/ramp-go

Requires Go 1.25+.


Quick Start

import (
    ramp "github.com/iamkanishka/ramp-go"
    "github.com/iamkanishka/ramp-go/domain/transaction"
)

client, err := ramp.New(ramp.Config{
    ClientID:     os.Getenv("RAMP_CLIENT_ID"),
    ClientSecret: os.Getenv("RAMP_CLIENT_SECRET"),
    Scopes:       []string{"transactions:read", "cards:read", "users:read"},
})
if err != nil {
    log.Fatal(err)
}

// Iterate all SYNC_READY transactions across all pages
syncReady := ramp.SyncStatusSyncReady
iter := client.Transactions.List(ctx, transaction.ListParams{
    SyncStatus: &syncReady,
})
for iter.Next(ctx) {
    txn := iter.Item()
    fmt.Println(txn.ID, txn.Amount, txn.MerchantName)
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}

Configuration

client, err := ramp.New(ramp.Config{
    ClientID:     "...",
    ClientSecret: "...",

    // Space-separated scopes or a []string — bound to the token at issuance
    Scopes: []string{"transactions:read", "cards:write", "users:read"},

    // Use ramp.SandboxBaseURL for sandbox testing
    BaseURL: ramp.SandboxBaseURL,

    // Per-request HTTP timeout (default: 30s)
    HTTPTimeout: 30 * time.Second,

    // Retry config (default: 3 retries, 500ms base, full-jitter backoff)
    MaxRetries:     3,
    RetryBaseDelay: 500 * time.Millisecond,

    // Bring your own slog.Logger (defaults to slog.Default())
    Logger: slog.New(slog.NewJSONHandler(os.Stdout, nil)),
})

Project Structure (DDD)

ramp-go/
├── ramp.go                 # Root client — assembles all domain services
├── errors.go               # ramp.Error, IsNotFound, IsRateLimit, IsAuth, IsValidation
├── types.go                # Re-exported shared types (Money, SyncStatus, etc.)
│
├── shared/                 # Zero-import shared value types (breaks import cycles)
│   ├── types.go            # Money, Address, SyncStatus, PagedResponse, DeferredTaskStatus
│   └── errors.go           # Error struct, ErrorType, APIError factory
│
├── domain/                 # One package per bounded context
│   ├── accounting/         # GL accounts, custom fields, connections, ERP sync
│   ├── auditlog/           # Audit event log
│   ├── bill/               # Accounts payable lifecycle
│   ├── business/           # Business entity read
│   ├── card/               # Card issuance, suspend, terminate (deferred)
│   ├── cashback/           # Cashback records
│   ├── department/         # Department CRUD
│   ├── entity/             # Multi-entity support
│   ├── limit/              # Spend controls (deferred create/terminate)
│   ├── location/           # Location CRUD
│   ├── merchant/           # Merchant read
│   ├── reimbursement/      # Reimbursement read
│   ├── spendprogram/       # Spend program read
│   ├── statement/          # Statement read
│   ├── transaction/        # Transaction list (all filters), get, update
│   ├── user/               # User CRUD, invite lifecycle (deferred)
│   ├── vendor/             # Vendor read
│   └── webhook/            # Webhook registration CRUD
│
├── webhooks/               # Standalone webhook handler (HMAC verify + dispatch)
│
├── internal/
│   ├── httpclient/         # HTTP transport — retry, backoff, 401 refresh, logging
│   ├── oauth/              # TokenManager — single-flight, proactive refresh
│   ├── pagination/         # Generic Iterator[T] — Next/Item/Err/Collect/Take
│   └── poller/             # Deferred task poller — typed Poll[T]
│
└── testutil/               # Test helpers — mock server, fixtures, WebhookSignature

Authentication

Client Credentials (default)

Tokens are fetched automatically on the first API call, cached, and refreshed 60 seconds before expiry. Concurrent callers share a single in-flight fetch (single-flight via sync.Cond).

Authorization Code (partner / multi-tenant)
// Redirect user to Ramp, then exchange the code:
if err := client.ExchangeAuthCode(ctx, code, "https://yourapp.com/callback"); err != nil {
    return err
}
// All subsequent calls use the exchanged token.
Pre-obtained Token
client, err := ramp.NewWithToken("ramp_tok_...", ramp.Config{
    ClientID:     "...",
    ClientSecret: "...",
})

Pagination

All list methods return *pagination.Iterator[T]:

// Lazy iteration — pages are fetched on demand
iter := client.Transactions.List(ctx, transaction.ListParams{})
for iter.Next(ctx) {
    txn := iter.Item()
    _ = txn
}
if err := iter.Err(); err != nil { /* handle */ }

// Collect all pages into a slice
all, err := client.Users.List(ctx, user.ListParams{}).Collect(ctx)

// Take at most N items (stops fetching after the page containing item N)
first10, err := client.Transactions.List(ctx, transaction.ListParams{}).Take(ctx, 10)

Cursor safety: Ramp uses cursor-only pagination. Take(ctx, 10) on a 100-item dataset fetches exactly 1 page — it never over-fetches.


Deferred Tasks

Card issuance, user creation, and limit creation are asynchronous — the API returns a task reference immediately. The SDK gives you full control:

// Option 1: fire-and-forget, get task ref, poll manually
ref, _, err := client.Cards.Create(ctx, card.CreateParams{
    DisplayName:    "AWS Infra",
    UserID:         "user-uuid",
    IdempotencyKey: uuid.New().String(),
}, nil) // nil opts = no polling

// Option 2: block until done (poll=true via opts)
_, newCard, err := client.Cards.Create(ctx, card.CreateParams{
    DisplayName:    "AWS Infra",
    UserID:         "user-uuid",
    IdempotencyKey: uuid.New().String(),
}, &card.PollOptions{
    IntervalMs:    500,
    MaxIntervalMs: 5_000,
    TimeoutMs:     60_000,
})

Webhooks

import "github.com/iamkanishka/ramp-go/webhooks"
import "github.com/iamkanishka/ramp-go/domain/webhook"

h := webhooks.NewHandler(os.Getenv("RAMP_WEBHOOK_SECRET"))

h.On(webhook.EventTransactionCreated, func(e webhooks.RawEvent) error {
    var txn transaction.Transaction
    if err := json.Unmarshal(e.Data, &txn); err != nil {
        return err
    }
    return syncToERP(txn)
}).On(webhook.EventCardSuspended, func(e webhooks.RawEvent) error {
    log.Printf("card suspended: %s", e.ID)
    return nil
}).OnAny(func(e webhooks.RawEvent) error {
    // Wildcard — fires for every event type
    metrics.Increment("ramp.webhook." + string(e.Type))
    return nil
})

// Register as http.Handler — works with any net/http compatible router
http.Handle("/webhooks/ramp", h)

The handler:

  • Verifies HMAC-SHA256 using the ramp-webhook-signature header
  • Rejects timestamps outside the 5-minute replay window
  • Dispatches to specific + wildcard handlers in registration order
  • Returns 401 on bad signature, 500 on handler error, 200 on success

For manual verification:

event, err := h.ConstructEvent(rawBody, signatureHeader)

Error Handling

All errors are *ramp.Error with a discriminated Type:

_, err := client.Users.Get(ctx, userID)
if err != nil {
    var re *ramp.Error
    if errors.As(err, &re) {
        switch re.Type {
        case ramp.ErrorTypeNotFound:
            // 404 — user doesn't exist
        case ramp.ErrorTypeRateLimit:
            log.Printf("rate limited, retry after %ds", re.RetryAfter)
        case ramp.ErrorTypeAuthentication:
            // 401 — refresh credentials
        case ramp.ErrorTypeAuthorization:
            // 403 — insufficient scope
        case ramp.ErrorTypeValidation:
            log.Printf("bad request: %s\nbody: %s", re.Message, re.Body)
        case ramp.ErrorTypeServer:
            log.Printf("ramp server error [trace=%s]", re.TraceID)
        }
    }
}

// Convenience predicates
if ramp.IsNotFound(err)   { /* 404 */ }
if ramp.IsRateLimit(err)  { /* 429 */ }
if ramp.IsAuth(err)       { /* 401 or 403 */ }
if ramp.IsValidation(err) { /* 400 */ }
Type HTTP Retried automatically
authentication_error 401 Once (token refresh + retry)
authorization_error 403 No
not_found 404 No
validation_error 400 No
rate_limit_error 429 Yes (up to MaxRetries)
server_error 5xx Yes (up to MaxRetries)
network_error Yes (up to MaxRetries)
timeout_error No
deferred_task_error No

ERP Sync Workflow

// 1. Fetch all objects ready to sync
syncReady := ramp.SyncStatusSyncReady
txns, err := client.Transactions.List(ctx, transaction.ListParams{
    SyncStatus: &syncReady,
    EntityID:   ramp.Ptr("entity-uuid"), // multi-entity support
}).Collect(ctx)

// 2. Process in your ERP...
for _, txn := range txns {
    if err := yourERP.Sync(txn); err != nil {
        // handle
    }
}

// 3. Report results back to Ramp
syncs := make([]accounting.SyncEntry, len(txns))
for i, txn := range txns {
    syncs[i] = accounting.SyncEntry{
        ObjectID:   txn.ID,
        ObjectType: accounting.SyncObjectTransaction,
        SyncStatus: accounting.SyncResultSuccess,
    }
}
err = client.Accounting.PostSyncStatus(ctx, accounting.PostSyncParams{
    IdempotencyKey: uuid.New().String(),
    Syncs:          syncs,
})

Sandbox

client, err := ramp.New(ramp.Config{
    ClientID:     os.Getenv("RAMP_SANDBOX_CLIENT_ID"),
    ClientSecret: os.Getenv("RAMP_SANDBOX_CLIENT_SECRET"),
    BaseURL:      ramp.SandboxBaseURL, // "https://demo-api.ramp.com"
})

Testing

Use testutil.NewServer to mock the Ramp API in your own tests:

import "github.com/iamkanishka/ramp-go/testutil"

func TestMyWorkflow(t *testing.T) {
    srv := testutil.NewServer(t) // auto-closes on t.Cleanup
    srv.QueueJSON(testutil.MakeUser(map[string]any{"email": "alice@example.com"}))

    client, _ := ramp.NewWithToken("test_token", ramp.Config{
        ClientID:     "id",
        ClientSecret: "secret",
        BaseURL:      srv.URL(),
    })
    u, err := client.Users.Get(context.Background(), "user-001")
    // assert...
}

Resource Reference

client.X Domain package Coverage
Accounting domain/accounting GL accounts, custom fields+options, connections, ERP sync
AuditLogs domain/auditlog Event log read
Bills domain/bill Create, list, get, update, void
Business domain/business Business entity read
Cards domain/card List, get, create (deferred), update, suspend, unsuspend, terminate
Cashbacks domain/cashback List, get
Departments domain/department List, get, create, update, delete
Limits domain/limit List, get, create (deferred), update, terminate (deferred)
Locations domain/location List, get, create, update, delete
Merchants domain/merchant List, get
Reimbursements domain/reimbursement List, get
SpendPrograms domain/spendprogram List, get
Statements domain/statement List, get
Transactions domain/transaction List (all filters), get, update memo/fields
Users domain/user List, get, create (deferred), update, deactivate, reactivate
Vendors domain/vendor List, get
Webhooks domain/webhook List, get, create, update, delete

License

MIT

Documentation

Overview

Package ramp provides a production-grade Go client for the Ramp Developer API v1.

Quick Start

client, err := ramp.New(ramp.Config{
    ClientID:     os.Getenv("RAMP_CLIENT_ID"),
    ClientSecret: os.Getenv("RAMP_CLIENT_SECRET"),
    Scopes:       []string{"transactions:read", "cards:read", "users:read"},
})
if err != nil {
    log.Fatal(err)
}

// Paginate all SYNC_READY transactions
iter := client.Transactions.List(ctx, transaction.ListParams{
    SyncStatus: ramp.Ptr(transaction.SyncStatusSyncReady),
})
for iter.Next(ctx) {
    txn := iter.Item()
    fmt.Println(txn.ID, txn.Amount)
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}

Package ramp re-exports shared types for consumer convenience. Consumers can use ramp.Money, ramp.SyncStatus etc. directly.

Index

Constants

View Source
const (
	ErrorTypeAuthentication = shared.ErrorTypeAuthentication
	ErrorTypeAuthorization  = shared.ErrorTypeAuthorization
	ErrorTypeNotFound       = shared.ErrorTypeNotFound
	ErrorTypeValidation     = shared.ErrorTypeValidation
	ErrorTypeRateLimit      = shared.ErrorTypeRateLimit
	ErrorTypeServer         = shared.ErrorTypeServer
	ErrorTypeTimeout        = shared.ErrorTypeTimeout
	ErrorTypeNetwork        = shared.ErrorTypeNetwork
	ErrorTypeDeferred       = shared.ErrorTypeDeferred
	ErrorTypeUnknown        = shared.ErrorTypeUnknown
)
View Source
const (
	// DefaultBaseURL is the Ramp production API base URL.
	DefaultBaseURL = "https://api.ramp.com"
	// SandboxBaseURL is the Ramp sandbox API base URL.
	SandboxBaseURL = "https://demo-api.ramp.com"
)
View Source
const (
	SyncStatusNotReady  = shared.SyncStatusNotReady
	SyncStatusSyncReady = shared.SyncStatusSyncReady
	SyncStatusSynced    = shared.SyncStatusSynced
	SyncStatusFailed    = shared.SyncStatusFailed
)
View Source
const (
	DeferredTaskStarted    = shared.DeferredTaskStarted
	DeferredTaskInProgress = shared.DeferredTaskInProgress
	DeferredTaskSuccess    = shared.DeferredTaskSuccess
	DeferredTaskError      = shared.DeferredTaskError
)

Variables

This section is empty.

Functions

func IsAuth

func IsAuth(err error) bool

IsAuth reports whether err is a Ramp authentication or authorization error.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a Ramp 404 error.

func IsRateLimit

func IsRateLimit(err error) bool

IsRateLimit reports whether err is a Ramp 429 rate-limit error.

func IsValidation

func IsValidation(err error) bool

IsValidation reports whether err is a Ramp 400 validation error.

func Ptr

func Ptr[T any](v T) *T

Ptr is a generic helper that returns a pointer to any value. Useful for setting optional fields in request structs:

params.Status = ramp.Ptr(card.StatusActive)

Types

type AccountingFieldSelection

type AccountingFieldSelection = shared.AccountingFieldSelection

AccountingFieldSelection represents a selected accounting dimension value.

type Address

type Address = shared.Address

Address is a physical mailing address.

type Client

type Client struct {
	// Accounting manages GL accounts, custom fields, sync operations, and ERP connections.
	Accounting *accounting.Service
	// AuditLogs provides read access to the audit event log.
	AuditLogs *auditlog.Service
	// Bills manages the accounts-payable bill lifecycle.
	Bills *bill.Service
	// Business provides read access to the business entity.
	Business *business.Service
	// Cards manages virtual and physical card issuance and lifecycle.
	Cards *card.Service
	// Cashbacks provides read access to cashback records.
	Cashbacks *cashback.Service
	// Departments manages department CRUD.
	Departments *department.Service
	// Limits manages spend controls (limits) CRUD and termination.
	Limits *limit.Service
	// Locations manages location CRUD.
	Locations *location.Service
	// Merchants provides read access to merchant records.
	Merchants *merchant.Service
	// Reimbursements provides read access to reimbursement records.
	Reimbursements *reimbursement.Service
	// SpendPrograms provides read access to spend program records.
	SpendPrograms *spendprogram.Service
	// Statements provides read access to billing statements.
	Statements *statement.Service
	// Transactions manages transaction reads and field updates.
	Transactions *transaction.Service
	// Users manages user CRUD, invitation lifecycle, and activation.
	Users *user.Service
	// Vendors provides read access to vendor records.
	Vendors *vendor.Service
	// Webhooks manages webhook registration CRUD.
	Webhooks *webhook.Service
	// contains filtered or unexported fields
}

Client is the root Ramp API client. All resource namespaces are fields. Client is safe for concurrent use by multiple goroutines.

func New

func New(cfg Config) (*Client, error)

New constructs a new Ramp client using the client_credentials OAuth2 flow. The client fetches and caches tokens automatically, refreshing before expiry.

func NewWithToken

func NewWithToken(token string, cfg Config) (*Client, error)

NewWithToken constructs a Ramp client with a pre-obtained access token. Useful for CLI tools, testing, or when managing token storage externally.

func (*Client) ExchangeAuthCode

func (c *Client) ExchangeAuthCode(ctx context.Context, code, redirectURI string) error

ExchangeAuthCode exchanges an OAuth2 authorization code for an access token. Use this for the partner / multi-tenant authorization_code flow.

func (*Client) RefreshToken

func (c *Client) RefreshToken(ctx context.Context) error

RefreshToken forces an immediate token refresh, bypassing the cache.

type Config

type Config struct {
	// ClientID is the OAuth2 client ID. Required.
	ClientID string
	// ClientSecret is the OAuth2 client secret. Required.
	ClientSecret string
	// Scopes is the list of OAuth2 scopes to request.
	Scopes []string
	// BaseURL overrides the default production API URL.
	// Use ramp.SandboxBaseURL for sandbox testing.
	BaseURL string
	// HTTPTimeout is the per-request HTTP timeout (default: 30s).
	HTTPTimeout time.Duration
	// MaxRetries is the maximum number of retry attempts on transient errors (default: 3).
	MaxRetries int
	// RetryBaseDelay is the base delay for exponential backoff (default: 500ms).
	RetryBaseDelay time.Duration
	// Logger is an optional structured logger. Defaults to slog.Default().
	Logger *slog.Logger
}

Config holds all configuration options for the Ramp client.

type DeferredTaskRef

type DeferredTaskRef = shared.DeferredTaskRef

DeferredTaskRef holds the ID of a submitted deferred task.

type DeferredTaskStatus

type DeferredTaskStatus = shared.DeferredTaskStatus

DeferredTaskStatus is the execution state of an asynchronous deferred operation.

type Error

type Error = shared.Error

Error is the structured error type returned by all SDK operations.

func APIError

func APIError(statusCode int, body []byte, traceID string) *Error

APIError constructs an Error from an HTTP response status and body.

type ErrorType

type ErrorType = shared.ErrorType

ErrorType classifies the category of a Ramp API error.

type ListParams

type ListParams = shared.ListParams

ListParams is the base struct for paginated list requests.

type Money

type Money = shared.Money

Money represents a monetary amount with currency.

type Page

type Page = shared.Page

Page holds cursor pagination metadata.

type PagedResponse

type PagedResponse[T any] struct {
	Data []T         `json:"data"`
	Page shared.Page `json:"page"`
}

PagedResponse is the generic envelope for all list endpoints. Note: this is a concrete re-declaration, not an alias, for Go 1.22 compatibility.

type PolicyViolation

type PolicyViolation = shared.PolicyViolation

PolicyViolation represents a policy rule that was violated on a transaction.

type SyncStatus

type SyncStatus = shared.SyncStatus

SyncStatus is the ERP sync state for a syncable object.

Directories

Path Synopsis
domain
accounting
Package accounting provides the Ramp Accounting domain service.
Package accounting provides the Ramp Accounting domain service.
auditlog
Package auditlog provides the Ramp Audit Logs domain service.
Package auditlog provides the Ramp Audit Logs domain service.
bill
Package bill provides the Ramp Bills (Accounts Payable) domain service.
Package bill provides the Ramp Bills (Accounts Payable) domain service.
business
Package business provides the Ramp Business domain service.
Package business provides the Ramp Business domain service.
card
Package card provides the Ramp Cards domain service.
Package card provides the Ramp Cards domain service.
cashback
Package cashback provides the Ramp Cashbacks domain service.
Package cashback provides the Ramp Cashbacks domain service.
department
Package department provides the Ramp Departments domain service.
Package department provides the Ramp Departments domain service.
entity
Package entity provides the Ramp Entities domain service.
Package entity provides the Ramp Entities domain service.
limit
Package limit provides the Ramp Limits (Spend Controls) domain service.
Package limit provides the Ramp Limits (Spend Controls) domain service.
location
Package location provides the Ramp Locations domain service.
Package location provides the Ramp Locations domain service.
merchant
Package merchant provides the Ramp Merchants domain service.
Package merchant provides the Ramp Merchants domain service.
reimbursement
Package reimbursement provides the Ramp Reimbursements domain service.
Package reimbursement provides the Ramp Reimbursements domain service.
spendprogram
Package spendprogram provides the Ramp Spend Programs domain service.
Package spendprogram provides the Ramp Spend Programs domain service.
statement
Package statement provides the Ramp Statements domain service.
Package statement provides the Ramp Statements domain service.
transaction
Package transaction provides the Ramp Transactions domain service.
Package transaction provides the Ramp Transactions domain service.
user
Package user provides the Ramp Users domain service.
Package user provides the Ramp Users domain service.
vendor
Package vendor provides the Ramp Vendors domain service.
Package vendor provides the Ramp Vendors domain service.
webhook
Package webhook provides the Ramp Webhooks domain service.
Package webhook provides the Ramp Webhooks domain service.
internal
httpclient
Package httpclient provides the HTTP transport layer for the Ramp SDK.
Package httpclient provides the HTTP transport layer for the Ramp SDK.
oauth
Package oauth implements OAuth 2.0 token management for the Ramp API.
Package oauth implements OAuth 2.0 token management for the Ramp API.
pagination
Package pagination provides a generic cursor-based page iterator.
Package pagination provides a generic cursor-based page iterator.
poller
Package poller implements async deferred-task polling for the Ramp API.
Package poller implements async deferred-task polling for the Ramp API.
Package shared contains value types shared across all ramp-go packages.
Package shared contains value types shared across all ramp-go packages.
Package testutil provides test helpers, mock HTTP servers, and fixtures for unit testing the ramp-go SDK without real API calls.
Package testutil provides test helpers, mock HTTP servers, and fixtures for unit testing the ramp-go SDK without real API calls.
Package webhooks provides HMAC-SHA256 webhook signature verification and typed event dispatch for Ramp webhook payloads.
Package webhooks provides HMAC-SHA256 webhook signature verification and typed event dispatch for Ramp webhook payloads.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL