/**
* @file Brex API Client
* @version 1.0.0
* @status STABLE - DO NOT MODIFY WITHOUT TESTS
* @lastModified 2024-03-19
*
* Brex API client implementation
*
* IMPORTANT:
* - Add tests for any new API endpoints
* - Handle rate limiting and errors appropriately
*
* Functionality:
* - Fetch accounts and transactions
* - Fetch and manage expenses
* - Upload and manage receipts
* - Manage budgets and spend limits
* - Handle authentication and errors
* - Implement rate limiting
*/
import axios, { AxiosInstance } from 'axios';
import { appConfig } from '../../config/index.js';
import { logError, logInfo, logWarn, logDebug } from '../../utils/logger.js';
import { BrexAccount, BrexTransaction, BrexPaginatedResponse } from './types.js';
import {
CardAccount,
CashAccount,
PageCashAccount,
PageStatement,
PageCardTransaction,
PageCashTransaction
} from './transactions-types.js';
import {
Expense,
SimpleExpense,
ExpensesResponse,
ListExpensesParams,
UpdateExpenseRequest,
ReceiptMatchRequest,
ReceiptUploadRequest,
CreateAsyncFileUploadResponse,
ExpenseStatus
} from './expenses-types.js';
import {
Budget,
BudgetListParams,
BudgetsResponse,
CreateBudgetRequest,
UpdateBudgetRequest,
SpendLimit,
SpendLimitListParams,
SpendLimitsResponse,
CreateSpendLimitRequest,
UpdateSpendLimitRequest,
BudgetProgram,
BudgetProgramListParams,
BudgetProgramsResponse,
CreateBudgetProgramRequest,
UpdateBudgetProgramRequest
} from '../../models/budget.js';
export class BrexClient {
private client: AxiosInstance;
constructor() {
if (!appConfig.brex.apiKey) {
logWarn('Brex API key is not set. API requests will fail with 401 Unauthorized.');
}
this.client = axios.create({
baseURL: appConfig.brex.apiUrl,
headers: {
'Authorization': `Bearer ${appConfig.brex.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': 'mcp-brex/0.2.2 (+https://github.com/dennisonbertram/mcp-brex)'
},
});
// Attach a request interceptor to add Idempotency-Key for writes if missing
this.client.interceptors.request.use((config) => {
const method = (config.method || 'get').toUpperCase();
// Generate a simple idempotency key for POST/PUT if caller didn't specify one
if ((method === 'POST' || method === 'PUT') && config.headers) {
const headers = config.headers as Record<string, string>;
if (!('Idempotency-Key' in headers) && !('idempotency-key' in headers)) {
try {
// Use crypto.randomUUID when available; otherwise, fallback
const key = (globalThis as any).crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
headers['Idempotency-Key'] = key;
} catch {
headers['Idempotency-Key'] = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
}
}
return config;
});
// Add response interceptor for logging
this.client.interceptors.response.use(
(response) => {
logInfo('Brex API request successful', {
method: response.config.method,
url: response.config.url,
status: response.status,
});
return response;
},
async (error) => {
// Handle rate limiting with simple retry/backoff
const status = error?.response?.status;
const cfg: any = error?.config || {};
if (status === 429 && cfg) {
cfg.__retryCount = cfg.__retryCount || 0;
if (cfg.__retryCount < 2) {
cfg.__retryCount += 1;
let delayMs = 1000;
const ra = error.response.headers?.['retry-after'];
if (ra) {
const asInt = parseInt(ra, 10);
if (!Number.isNaN(asInt)) {
delayMs = asInt * 1000;
} else {
const raDate = Date.parse(ra);
if (!Number.isNaN(raDate)) {
delayMs = Math.max(0, raDate - Date.now());
}
}
}
logWarn(`Brex API rate limited (429). Retrying in ${delayMs}ms...`, { url: cfg.url, attempt: cfg.__retryCount });
await new Promise((res) => setTimeout(res, delayMs));
return this.client.request(cfg);
}
}
if (axios.isAxiosError(error) && error.response?.status === 401) {
logError(`Brex API authentication failed: Invalid or expired API key. Check your BREX_API_KEY in .env file.`, {
method: error.config?.method,
url: error.config?.url,
status: error.response?.status,
});
} else {
logError(error, {
method: error.config?.method,
url: error.config?.url,
status: error.response?.status,
});
}
throw error;
}
);
}
// Accounts API
async getAccounts(): Promise<BrexPaginatedResponse<BrexAccount>> {
try {
logDebug('Fetching all accounts (cash and card)');
// Fetch both cash and card accounts in parallel
const [cashAccountsResponse, cardAccounts] = await Promise.all([
this.client.get('/v2/accounts/cash'),
this.getCardAccounts()
]);
const accounts: BrexAccount[] = [];
// Add cash accounts
if (cashAccountsResponse.data && cashAccountsResponse.data.items) {
for (const cashAccount of cashAccountsResponse.data.items) {
accounts.push({
id: cashAccount.id,
name: cashAccount.name,
type: 'CASH' as const,
currency: cashAccount.current_balance.currency || 'USD',
balance: {
amount: cashAccount.current_balance.amount,
currency: cashAccount.current_balance.currency || 'USD'
},
status: cashAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE'
});
}
}
// Add card accounts
for (const cardAccount of cardAccounts) {
accounts.push({
id: cardAccount.id,
name: `Card Account ${cardAccount.id}`,
type: 'CARD' as const,
currency: cardAccount.current_balance?.currency || 'USD',
balance: {
amount: cardAccount.current_balance?.amount || 0,
currency: cardAccount.current_balance?.currency || 'USD'
},
status: cardAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE'
});
}
return {
items: accounts,
hasMore: false // We fetched all accounts
};
} catch (error) {
this.handleApiError(error, 'GET', '/accounts');
throw error;
}
}
async getAccount(accountId: string): Promise<BrexAccount> {
try {
// Determine account type based on ID prefix
if (accountId.startsWith('dpacc_')) {
// This is a cash account - use the cash endpoint
logDebug(`Fetching cash account ${accountId}`);
const response = await this.client.get(`/v2/accounts/cash/${accountId}`);
const cashAccount = response.data;
// Transform to BrexAccount format
return {
id: cashAccount.id,
name: cashAccount.name,
type: 'CASH' as const,
currency: cashAccount.current_balance.currency || 'USD',
balance: {
amount: cashAccount.current_balance.amount,
currency: cashAccount.current_balance.currency || 'USD'
},
status: cashAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE'
};
} else if (accountId.startsWith('cuacc_')) {
// This is a card account - get from the card accounts list
logDebug(`Fetching card account ${accountId}`);
const cardAccounts = await this.getCardAccounts();
const cardAccount = cardAccounts.find(account => account.id === accountId);
if (!cardAccount) {
throw new Error(`Card account ${accountId} not found`);
}
// Transform to BrexAccount format
return {
id: cardAccount.id,
name: `Card Account ${cardAccount.id}`, // Card accounts don't have names
type: 'CARD' as const,
currency: cardAccount.current_balance?.currency || 'USD',
balance: {
amount: cardAccount.current_balance?.amount || 0,
currency: cardAccount.current_balance?.currency || 'USD'
},
status: cardAccount.status === 'ACTIVE' ? 'ACTIVE' : 'INACTIVE'
};
} else {
throw new Error(`Unknown account ID format: ${accountId}. Expected account ID to start with 'dpacc_' (cash) or 'cuacc_' (card)`);
}
} catch (error) {
this.handleApiError(error, 'GET', `/account/${accountId}`);
throw error;
}
}
/**
* Get card accounts
* @returns List of card accounts
*/
async getCardAccounts(): Promise<CardAccount[]> {
try {
logDebug('Fetching card accounts from Brex API');
const response = await this.client.get('/v2/accounts/card');
logDebug(`Successfully fetched ${response.data.length} card accounts`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', '/v2/accounts/card');
throw error;
}
}
/**
* Get primary card account statements
* @param cursor Pagination cursor
* @param limit Number of items per page
* @returns Paginated list of statements
*/
async getPrimaryCardStatements(cursor?: string, limit?: number): Promise<PageStatement> {
try {
const params: Record<string, unknown> = {};
if (cursor) params.cursor = cursor;
if (limit) params.limit = limit;
logDebug('Fetching primary card account statements from Brex API');
const response = await this.client.get('/v2/accounts/card/primary/statements', { params });
logDebug(`Successfully fetched ${response.data.items.length} primary card statements`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', '/v2/accounts/card/primary/statements');
throw error;
}
}
/**
* Get cash accounts
* @returns Paginated list of cash accounts
*/
async getCashAccounts(): Promise<PageCashAccount> {
try {
logDebug('Fetching cash accounts from Brex API');
const response = await this.client.get('/v2/accounts/cash');
logDebug(`Successfully fetched ${response.data.items.length} cash accounts`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', '/v2/accounts/cash');
throw error;
}
}
/**
* Get primary cash account
* @returns Primary cash account
*/
async getPrimaryCashAccount(): Promise<CashAccount> {
try {
logDebug('Fetching primary cash account from Brex API');
const response = await this.client.get('/v2/accounts/cash/primary');
logDebug('Successfully fetched primary cash account');
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', '/v2/accounts/cash/primary');
throw error;
}
}
/**
* Get cash account by ID
* @param id Account ID
* @returns Cash account
*/
async getCashAccountById(id: string): Promise<CashAccount> {
try {
logDebug(`Fetching cash account ${id} from Brex API`);
const response = await this.client.get(`/v2/accounts/cash/${id}`);
logDebug(`Successfully fetched cash account ${id}`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', `/v2/accounts/cash/${id}`);
throw error;
}
}
/**
* Get cash account statements
* @param id Account ID
* @param cursor Pagination cursor
* @param limit Number of items per page
* @returns Paginated list of statements
*/
async getCashAccountStatements(id: string, cursor?: string, limit?: number): Promise<PageStatement> {
try {
const params: Record<string, unknown> = {};
if (cursor) params.cursor = cursor;
if (limit) params.limit = limit;
logDebug(`Fetching statements for cash account ${id} from Brex API`);
const response = await this.client.get(`/v2/accounts/cash/${id}/statements`, { params });
logDebug(`Successfully fetched ${response.data.items.length} statements for cash account ${id}`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', `/v2/accounts/cash/${id}/statements`);
throw error;
}
}
/**
* Get card transactions
* @param options Options for fetching card transactions
* @returns Paginated list of card transactions
*/
async getCardTransactions(options?: {
cursor?: string;
limit?: number;
user_ids?: string[];
}): Promise<PageCardTransaction> {
try {
const params: Record<string, unknown> = {};
if (options) {
if (options.cursor) params.cursor = options.cursor;
if (options.limit) params.limit = options.limit;
if (options.user_ids && options.user_ids.length > 0) params.user_ids = options.user_ids;
// Note: posted_at_start and expand are NOT supported for card transactions; omit to avoid 400s
}
logDebug('Fetching card transactions from Brex API');
const response = await this.client.get('/v2/transactions/card/primary', { params });
logDebug(`Successfully fetched ${response.data.items.length} card transactions`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', '/v2/transactions/card/primary');
throw error;
}
}
/**
* Get cash transactions
* @param id Account ID
* @param options Options for fetching cash transactions
* @returns Paginated list of cash transactions
*/
async getCashTransactions(id: string, options?: {
cursor?: string;
limit?: number;
}): Promise<PageCashTransaction> {
try {
const params: Record<string, unknown> = {};
if (options) {
if (options.cursor) params.cursor = options.cursor;
if (options.limit) params.limit = options.limit;
// Note: posted_at_start and expand are NOT supported for cash transactions; omit to avoid 400s
}
logDebug(`Fetching cash transactions for account ${id} from Brex API`);
const response = await this.client.get(`/v2/transactions/cash/${id}`, { params });
logDebug(`Successfully fetched ${response.data.items.length} cash transactions for account ${id}`);
return response.data;
} catch (error) {
this.handleApiError(error, 'GET', `/v2/transactions/cash/${id}`);
throw error;
}
}
// Transactions API
async getTransactions(
accountId: string,
cursor?: string,
limit: number = 50
): Promise<BrexPaginatedResponse<BrexTransaction>> {
try {
logDebug(`Fetching transactions for account ${accountId}`);
if (accountId.startsWith('dpacc_')) {
// Cash account - use cash transactions endpoint
const response = await this.getCashTransactions(accountId, {
cursor,
limit
});
// Transform to BrexTransaction format
const transactions: BrexTransaction[] = response.items.map((tx) => ({
id: tx.id,
date: tx.posted_at_date,
description: tx.description,
amount: {
amount: tx.amount?.amount || 0,
currency: tx.amount?.currency || 'USD'
},
status: 'POSTED' as const,
type: (tx.amount?.amount || 0) >= 0 ? 'CREDIT' : 'DEBIT',
category: tx.type,
accountId: accountId
}));
return {
items: transactions,
nextCursor: response.next_cursor,
hasMore: Boolean(response.next_cursor)
};
} else if (accountId.startsWith('cuacc_')) {
// Card account - use card transactions endpoint
const response = await this.getCardTransactions({
cursor,
limit
});
// Transform to BrexTransaction format
const transactions: BrexTransaction[] = response.items.map((tx) => ({
id: tx.id,
date: tx.posted_at_date,
description: tx.description,
amount: {
amount: tx.amount.amount,
currency: tx.amount.currency || 'USD'
},
status: 'POSTED' as const,
type: 'DEBIT' as const, // Card transactions are typically debits
category: tx.type,
merchant: tx.merchant ? {
name: tx.merchant.raw_descriptor,
category: tx.merchant.mcc
} : undefined,
accountId: accountId
}));
return {
items: transactions,
nextCursor: response.next_cursor,
hasMore: Boolean(response.next_cursor)
};
} else {
throw new Error(`Unknown account ID format: ${accountId}. Expected account ID to start with 'dpacc_' (cash) or 'cuacc_' (card)`);
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
}
throw error;
}
}
async getTransaction(_transactionId: string): Promise<BrexTransaction> {
try {
// This is a fallback implementation since we couldn't find a direct transaction endpoint
throw new Error('Individual transaction endpoint not available');
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
}
throw error;
}
}
// User API
async getCurrentUser(): Promise<any> {
try {
const response = await this.client.get('/v2/users/me');
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
}
throw error;
}
}
// Expenses API
async getExpenses(params?: ListExpensesParams): Promise<ExpensesResponse> {
try {
logDebug('Fetching expenses from Brex API');
const query: Record<string, unknown> = {};
if (params) {
if (params.expand !== undefined) query['expand[]'] = params.expand;
if (params.user_id) query['user_id[]'] = params.user_id;
if (params.parent_expense_id) query['parent_expense_id[]'] = params.parent_expense_id;
if (params.budget_id) query['budget_id[]'] = params.budget_id;
if (params.spending_entity_id) query['spending_entity_id[]'] = params.spending_entity_id;
if (params.expense_type) query['expense_type[]'] = params.expense_type;
if (params.status) query['status[]'] = params.status;
if (params.payment_status) query['payment_status[]'] = params.payment_status;
if (params.purchased_at_start) query.purchased_at_start = params.purchased_at_start;
if (params.purchased_at_end) query.purchased_at_end = params.purchased_at_end;
if (params.updated_at_start) query.updated_at_start = params.updated_at_start;
if (params.updated_at_end) query.updated_at_end = params.updated_at_end;
if (params.load_custom_fields !== undefined) query.load_custom_fields = params.load_custom_fields;
if (params.cursor) query.cursor = params.cursor;
if (params.limit) query.limit = params.limit;
}
const response = await this.client.get('/v1/expenses', { params: query });
const data = (response.data || {}) as { items?: unknown[]; next_cursor?: string };
const normalized = {
items: Array.isArray(data.items) ? data.items : [],
nextCursor: data.next_cursor || '',
hasMore: Boolean(data.next_cursor)
} as ExpensesResponse;
return normalized;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
}
throw error;
}
}
async getExpense(expenseId: string, params?: { expand?: string[], load_custom_fields?: boolean }): Promise<Expense> {
try {
logDebug(`Fetching expense ${expenseId} from Brex API`);
const response = await this.client.get(`/v1/expenses/${expenseId}`, { params });
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
} else if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new Error(`Expense with ID ${expenseId} not found`);
}
throw error;
}
}
// Card Expenses API
async getCardExpenses(params?: ListExpensesParams): Promise<ExpensesResponse> {
try {
logDebug('Fetching card expenses from Brex API');
try {
// Build query with correct bracketed array keys - respect user's expand choices
const query: Record<string, unknown> = {};
if (params?.expand !== undefined) query['expand[]'] = params.expand;
if (params?.user_id) query['user_id[]'] = params.user_id;
if (params?.parent_expense_id) query['parent_expense_id[]'] = params.parent_expense_id;
if (params?.budget_id) query['budget_id[]'] = params.budget_id;
if (params?.spending_entity_id) query['spending_entity_id[]'] = params.spending_entity_id;
// Do NOT send expense_type to /card endpoint; filter is implied
if (params?.status) query['status[]'] = params.status;
if (params?.payment_status) query['payment_status[]'] = params.payment_status;
if (params?.purchased_at_start) query.purchased_at_start = params.purchased_at_start;
if (params?.purchased_at_end) query.purchased_at_end = params.purchased_at_end;
if (params?.updated_at_start) query.updated_at_start = params.updated_at_start;
if (params?.updated_at_end) query.updated_at_end = params.updated_at_end;
if (params?.load_custom_fields !== undefined) query.load_custom_fields = params.load_custom_fields;
if (params?.cursor) query.cursor = params.cursor;
if (params?.limit) query.limit = params.limit;
const response = await this.client.get('/v1/expenses/card', { params: query });
const data = (response.data || {}) as { items?: unknown[]; next_cursor?: string };
if (!data || !Array.isArray(data.items)) {
logWarn('Unexpected response format from Brex Card Expenses API');
}
const normalized = {
items: Array.isArray(data.items) ? data.items : [],
nextCursor: data.next_cursor || '',
hasMore: Boolean(data.next_cursor)
} as ExpensesResponse;
return normalized;
} catch (apiError) {
logError(`Brex Card Expenses API error: ${apiError instanceof Error ? apiError.message : String(apiError)}`);
// Return empty but valid response
return {
items: [],
nextCursor: '',
hasMore: false
};
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
logWarn(`Brex API authentication failed: Please check your API key in the .env file`);
}
// Always return a valid response
return {
items: [],
nextCursor: '',
hasMore: false
};
}
}
async getCardExpense(expenseId: string, params?: { expand?: string[], load_custom_fields?: boolean }): Promise<Expense> {
try {
logDebug(`Fetching card expense ${expenseId} from Brex API`);
try {
const response = await this.client.get(`/v1/expenses/card/${expenseId}`, { params });
// Handle potential response format issues
if (!response.data || typeof response.data !== 'object') {
logWarn(`Unexpected response format for card expense ${expenseId}`);
// Create a minimal valid expense object
return {
id: expenseId,
updated_at: new Date().toISOString(),
status: ExpenseStatus.DRAFT,
...(response.data && typeof response.data === 'object' ? response.data : {})
};
}
// Ensure the response has the required fields
if (!response.data.id) {
logDebug(`Adding id to card expense response: ${expenseId}`);
response.data.id = expenseId;
}
if (!response.data.updated_at) {
logDebug(`Adding updated_at to card expense response`);
response.data.updated_at = new Date().toISOString();
}
// Log the response structure
logDebug(`Card expense response structure: ${JSON.stringify(Object.keys(response.data))}`)
return response.data;
} catch (apiError) {
logError(`Brex Card Expense API error: ${apiError instanceof Error ? apiError.message : String(apiError)}`);
// Return a valid minimal expense object
return {
id: expenseId,
updated_at: new Date().toISOString(),
status: ExpenseStatus.DRAFT,
memo: `Error: ${apiError instanceof Error ? apiError.message : String(apiError)}`
};
}
} catch (error) {
logWarn(`General error fetching card expense ${expenseId}: ${error instanceof Error ? error.message : String(error)}`);
// Always return a valid expense object
return {
id: expenseId,
updated_at: new Date().toISOString(),
status: ExpenseStatus.DRAFT,
memo: `Error: ${error instanceof Error ? error.message : String(error)}`
};
}
}
async updateCardExpense(expenseId: string, updateData: UpdateExpenseRequest): Promise<SimpleExpense> {
try {
logDebug(`Updating card expense ${expenseId} in Brex API`);
const response = await this.client.put(`/v1/expenses/card/${expenseId}`, updateData);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
} else if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new Error(`Card expense with ID ${expenseId} not found`);
}
throw error;
}
}
// Receipt API
async createReceiptMatch(request: ReceiptMatchRequest): Promise<CreateAsyncFileUploadResponse> {
try {
logDebug('Creating receipt match in Brex API');
const response = await this.client.post('/v1/expenses/card/receipt_match', request);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
}
throw error;
}
}
async createReceiptUpload(expenseId: string, request: ReceiptUploadRequest): Promise<CreateAsyncFileUploadResponse> {
try {
logDebug(`Creating receipt upload for expense ${expenseId} in Brex API`);
const response = await this.client.post(`/v1/expenses/card/${expenseId}/receipt_upload`, request);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
throw new Error(`Brex API authentication failed: Please check your API key in the .env file`);
} else if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new Error(`Card expense with ID ${expenseId} not found`);
}
throw error;
}
}
// File upload helper
async uploadFileToS3(url: string, fileContent: Buffer, contentType: string): Promise<void> {
try {
logDebug('Uploading file to pre-signed S3 URL');
await axios.put(url, fileContent, {
headers: {
'Content-Type': contentType,
},
});
logDebug('File uploaded successfully');
} catch (error) {
logError('Failed to upload file to S3', { error });
throw new Error('Failed to upload file: ' + (error instanceof Error ? error.message : String(error)));
}
}
// Budget API methods
/**
* Get a list of budgets
* @param params Optional parameters for filtering and pagination
* @returns Paginated list of budgets
*/
async getBudgets(params?: BudgetListParams): Promise<BudgetsResponse> {
try {
logDebug('Fetching budgets from Brex API', { params });
return await this.get<BudgetsResponse>('/v2/budgets', params as unknown as Record<string, any>);
} catch (error) {
logError(`Error fetching budgets: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Get a single budget by ID
* @param budgetId Budget ID
* @returns Budget object
*/
async getBudget(budgetId: string): Promise<Budget> {
try {
logDebug(`Fetching budget ${budgetId} from Brex API`);
return await this.get<Budget>(`/v2/budgets/${budgetId}`);
} catch (error) {
logError(`Error fetching budget ${budgetId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Create a new budget
* @param data Budget creation request data
* @returns Created budget
*/
async createBudget(data: CreateBudgetRequest): Promise<Budget> {
try {
logDebug('Creating new budget in Brex API');
return await this.post<Budget>('/v2/budgets', data);
} catch (error) {
logError(`Error creating budget: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Update an existing budget
* @param budgetId Budget ID
* @param data Budget update request data
* @returns Updated budget
*/
async updateBudget(budgetId: string, data: UpdateBudgetRequest): Promise<Budget> {
try {
logDebug(`Updating budget ${budgetId} in Brex API`);
return await this.put<Budget>(`/v2/budgets/${budgetId}`, data);
} catch (error) {
logError(`Error updating budget ${budgetId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Archive a budget
* @param budgetId Budget ID
* @returns Archived budget
*/
async archiveBudget(budgetId: string): Promise<Budget> {
try {
logDebug(`Archiving budget ${budgetId} in Brex API`);
return await this.post<Budget>(`/v2/budgets/${budgetId}/archive`, {});
} catch (error) {
logError(`Error archiving budget ${budgetId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
// Spend Limits API methods
/**
* Get a list of spend limits
* @param params Optional parameters for filtering and pagination
* @returns Paginated list of spend limits
*/
async getSpendLimits(params?: SpendLimitListParams): Promise<SpendLimitsResponse> {
try {
logDebug('Fetching spend limits from Brex API', { params });
return await this.get<SpendLimitsResponse>('/v2/spend_limits', params as unknown as Record<string, any>);
} catch (error) {
logError(`Error fetching spend limits: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Get a single spend limit by ID
* @param spendLimitId Spend limit ID
* @returns Spend limit object
*/
async getSpendLimit(spendLimitId: string): Promise<SpendLimit> {
try {
logDebug(`Fetching spend limit ${spendLimitId} from Brex API`);
return await this.get<SpendLimit>(`/v2/spend_limits/${spendLimitId}`);
} catch (error) {
logError(`Error fetching spend limit ${spendLimitId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Create a new spend limit
* @param data Spend limit creation request data
* @returns Created spend limit
*/
async createSpendLimit(data: CreateSpendLimitRequest): Promise<SpendLimit> {
try {
logDebug('Creating new spend limit in Brex API');
return await this.post<SpendLimit>('/v2/spend_limits', data);
} catch (error) {
logError(`Error creating spend limit: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Update an existing spend limit
* @param spendLimitId Spend limit ID
* @param data Spend limit update request data
* @returns Updated spend limit
*/
async updateSpendLimit(spendLimitId: string, data: UpdateSpendLimitRequest): Promise<SpendLimit> {
try {
logDebug(`Updating spend limit ${spendLimitId} in Brex API`);
return await this.put<SpendLimit>(`/v2/spend_limits/${spendLimitId}`, data);
} catch (error) {
logError(`Error updating spend limit ${spendLimitId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Archive a spend limit
* @param spendLimitId Spend limit ID
* @returns Archived spend limit
*/
async archiveSpendLimit(spendLimitId: string): Promise<SpendLimit> {
try {
logDebug(`Archiving spend limit ${spendLimitId} in Brex API`);
return await this.post<SpendLimit>(`/v2/spend_limits/${spendLimitId}/archive`, {});
} catch (error) {
logError(`Error archiving spend limit ${spendLimitId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
// Budget Programs API methods
/**
* Get a list of budget programs
* @param params Optional parameters for filtering and pagination
* @returns Paginated list of budget programs
*/
async getBudgetPrograms(params?: BudgetProgramListParams): Promise<BudgetProgramsResponse> {
try {
logDebug('Fetching budget programs from Brex API', { params });
return await this.get<BudgetProgramsResponse>('/v1/budget_programs', params as unknown as Record<string, any>);
} catch (error) {
logError(`Error fetching budget programs: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Get a single budget program by ID
* @param programId Budget program ID
* @returns Budget program object
*/
async getBudgetProgram(programId: string): Promise<BudgetProgram> {
try {
logDebug(`Fetching budget program ${programId} from Brex API`);
return await this.get<BudgetProgram>(`/v1/budget_programs/${programId}`);
} catch (error) {
logError(`Error fetching budget program ${programId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Create a new budget program
* @param data Budget program creation request data
* @returns Created budget program
*/
async createBudgetProgram(data: CreateBudgetProgramRequest): Promise<BudgetProgram> {
try {
logDebug('Creating new budget program in Brex API');
return await this.post<BudgetProgram>('/v1/budget_programs', data);
} catch (error) {
logError(`Error creating budget program: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Update an existing budget program
* @param programId Budget program ID
* @param data Budget program update request data
* @returns Updated budget program
*/
async updateBudgetProgram(programId: string, data: UpdateBudgetProgramRequest): Promise<BudgetProgram> {
try {
logDebug(`Updating budget program ${programId} in Brex API`);
return await this.put<BudgetProgram>(`/v1/budget_programs/${programId}`, data);
} catch (error) {
logError(`Error updating budget program ${programId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Delete a budget program
* @param programId Budget program ID
* @returns Void
*/
async deleteBudgetProgram(programId: string): Promise<void> {
try {
logDebug(`Deleting budget program ${programId} from Brex API`);
await this.client.delete(`/v1/budget_programs/${programId}`);
} catch (error) {
logError(`Error deleting budget program ${programId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
// General API methods for flexible endpoint access
/**
* Generic GET method for Brex API
* @param endpoint API endpoint path (starting with /)
* @param params Optional query parameters
* @returns Response data
*/
async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> {
try {
logDebug(`Making GET request to Brex API endpoint: ${endpoint}`, { params });
const response = await this.client.get(endpoint, { params });
return response.data as T;
} catch (error) {
this.handleApiError(error, 'GET', endpoint);
throw error;
}
}
/**
* Generic POST method for Brex API
* @param endpoint API endpoint path (starting with /)
* @param data Request body data
* @param params Optional query parameters
* @returns Response data
*/
async post<T = any>(endpoint: string, data: unknown, params?: Record<string, any>): Promise<T> {
try {
logDebug(`Making POST request to Brex API endpoint: ${endpoint}`, { params });
const response = await this.client.post(endpoint, data, { params });
return response.data as T;
} catch (error) {
this.handleApiError(error, 'POST', endpoint);
throw error;
}
}
/**
* Generic PUT method for Brex API
* @param endpoint API endpoint path (starting with /)
* @param data Request body data
* @param params Optional query parameters
* @returns Response data
*/
async put<T = any>(endpoint: string, data: unknown, params?: Record<string, any>): Promise<T> {
try {
logDebug(`Making PUT request to Brex API endpoint: ${endpoint}`, { params });
const response = await this.client.put(endpoint, data, { params });
return response.data as T;
} catch (error) {
this.handleApiError(error, 'PUT', endpoint);
throw error;
}
}
/**
* Handle API errors consistently
* @param error The error object
* @param method HTTP method used
* @param endpoint API endpoint path
*/
private handleApiError(error: unknown, method: string, endpoint: string): void {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
logError(`Brex API authentication failed: Please check your API key in the .env file`, {
method,
endpoint,
status: error.response.status
});
} else if (error.response?.status === 404) {
logError(`Resource not found at ${endpoint}`, {
method,
endpoint,
status: error.response.status
});
} else {
logError(`Brex API error: ${error.response?.data?.message || error.message}`, {
method,
endpoint,
status: error.response?.status
});
}
} else {
logError(`Unexpected error during ${method} request to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}