List expenses with comprehensive filtering, OR aggregate them by group. Use this for personal expense queries, company-wide financial analysis, AND group-by/aggregation analytics ("total spend by vendor", "monthly burn", "top departments by spend", "expense count by status").
SPEND TERMINOLOGY:
- Spend limits are spending controls that exist independently of cards and can be assigned to expenses.
- Employee card limits (USER_LIMIT in tool results) are personal limits used by employee cards. An employee card is the card itself, not its limit; employee cards can also use separately issued spend limits.
- Budgets are separate planning/tracking containers that can contain spend limits and other budgets. A containing Budget is not the expense's assigned spend limit. budget_path describes visible containing budgets.
- Spend limit groups are templates giving each eligible member their own spend limit, not shared funds. A group is neither a spend limit nor a Budget; its parent_budget is its containing Budget.
- If a user says "budget", use the conversation and returned object types/path to distinguish a spend limit from a containing Budget. If the distinction remains ambiguous and changes the answer, ask which they mean; do not silently substitute one for the other.
- Use these product names in replies. Field names and ID prefixes such as budget_* do not establish the object's product type. Use returned IDs unchanged for tool calls and identify objects by name in replies.
EXPENSE ATTRIBUTION AND LIMIT ROUTING:
- Listing results include spend_limit when available. It has an ID/name but no object type or containing-budget path; do not infer a spend limit versus employee card limit versus Budget from this field's name alone.
- "How much did I spend on my Travel limit?" → resolve the spend limit with list_my_limits, then call this tool with expense_owner:"ME", limit_ids:[the returned ID], the requested date range, and aggregations:[{func:"SUM",field:"billing_amount",alias:"total_spend"}]. Omit group_by for a single total.
- For managed spend limits, resolve names with list_spend_limits instead. Match the returned names and use budget_path to disambiguate duplicate names. Follow pagination as needed; do not guess IDs.
- For a containing Budget, use its returned ID from budget_path. If discovery is needed, list_spend_limits exposes visible paths; its search parameter searches spend-limit names, not containing Budget names. limit_ids includes expenses assigned to the specified spend limits and descendants of specified containing Budgets. Pass the containing Budget ID once rather than summing overlapping parent/child queries. If the Budget cannot be resolved, ask for clarification rather than inventing an ID or claiming zero spend.
- "My spend" means expense_owner:"ME"; for spend across everyone on a limit or Budget omit expense_owner and user_ids (subject to access).
- Employee card limit amounts/available balances belong to list_my_limits (own) or list_employee_card_limits (account-wide). Do not assume limit_ids supports employee card limit IDs, or substitute user_ids: expenses owned by a user can be assigned to different limits.
- group_by:["budget"] is the existing name-based expense grouping key, not a containing-Budget hierarchy rollup. It can combine identical names and does not provide object types or paths. For a total under a resolved containing Budget, use limit_ids plus SUM without group_by.
PREFER THIS TOOL for any expense question that can be answered with filter + group_by + SUM/COUNT. That covers: "total spend by X", "top N by spend", "count of expenses by X", "monthly/weekly/daily breakdowns", "spend per vendor/department/category/user/budget/expense_type". Reserve query_expense_analytics for higher-order analytics (anomaly detection, trends quarter-over-quarter, forecasting, recommendations, vendor benchmarking) — use it only when the question genuinely requires those capabilities.
Two modes — same filters, different output:
- LISTING mode (default): omit aggregations → returns a paginated list of expenses
- AGGREGATION mode: supply aggregations (with or without group_by) → returns one row per group with computed totals/counts, or a single row when group_by is omitted. Use this for any "how many", "what's the total", "by", "per", "sum", "count", or "breakdown" question.
SINGLE-NUMBER QUESTIONS ("how many", "what's the total") — use AGGREGATION mode with no group_by: For any question asking for ONE number (no breakdown), supply aggregations and OMIT group_by. The result is a one-row aggregation envelope keyed by your aliases.
- "How many expenses missing receipts?" → receipt_status:"RECEIPT_ABSENT", aggregations:[{func:"COUNT", field:"all", alias:"missing_receipts"}]
- "How many submitted card expenses?" → types:["CARD"], approval_statuses:["SUBMITTED"], aggregations:[{func:"COUNT", field:"all", alias:"submitted_count"}]
- "How many expenses missing memo OR missing receipt?" → documentation_missing_any:["MEMO","RECEIPT"], aggregations:[{func:"COUNT", field:"all", alias:"missing_docs"}]
- "What's our total card spend this quarter?" → types:["CARD"], purchased_at_start/end for the quarter, aggregations:[{func:"SUM", field:"billing_amount", alias:"total_spend"}]
- "What's the total amount of CLEARED transactions in May?" → payment_statuses:["CLEARED"], purchased_at_start/end for May, aggregations:[{func:"SUM", field:"billing_amount", alias:"total"}]
- SUM returns a multi-currency array (e.g. ["1234.00 USD","56.78 CAD"]) when expenses span multiple currencies — present each bucket separately or convert client-side.
BREAKDOWN QUESTIONS ("by", "per", "top N") — use AGGREGATION mode with group_by:
- "Total spend by vendor this quarter" → group_by:["vendor"], aggregations:[{func:"SUM", field:"billing_amount", alias:"total_spend"}]
- "List each vendor with its spend" / "spend for every vendor" / "vendors ranked by spend" → group_by:["vendor"], aggregations:[{func:"SUM", field:"billing_amount", alias:"total_spend"}]. Use THIS tool (not list_vendors — that only lists vendor records and has no spend) whenever the ask pairs vendors/departments/categories/users with an amount or count.
- "Monthly card burn this year" → types:["CARD"], group_by:["month"], aggregations:[{func:"SUM", field:"billing_amount", alias:"total_spend"}]
- "Top 10 departments by spend" → group_by:["department"], aggregations:[{func:"SUM", field:"billing_amount", alias:"total_spend"}], limit:10
- "How many expenses are missing receipts, BY USER?" → receipt_status:"RECEIPT_ABSENT", group_by:["user_first_name","user_last_name"], aggregations:[{func:"COUNT", field:"all", alias:"expense_count"}]
- "Expense counts by category" → group_by:["category"], aggregations:[{func:"COUNT", field:"all", alias:"expense_count"}]
RANKING / ORDER (important for "top N by spend"):
- When you pass sort_by, rows come back already sorted — present them in that order. If ranked_by.basis is set, the ranking is a USD-normalized total that nets multiple currencies together, so the per-currency amounts shown are NOT the sort key — never re-order by a single currency's figure (it produces a wrong order).
EXTREMA — a SINGLE most/least expensive expense (not a total/breakdown): use LISTING mode with order_by:"amount", order_direction DESC (most) or ASC (least), limit:1. Do NOT use the default recency sort — it returns the most RECENT expense, not the highest-dollar one. order_by:"amount" ranks by the USD-normalized amount shown per row, so it's correct across mixed currencies.
CUSTOM AMOUNT BUCKETS (e.g. "<$25, $25-$75, $75-$250, ..."): For numeric range bucketing, make N targeted calls with min_amount/max_amount filters per bucket (each with COUNT aggregation, no group_by) and combine the results client-side.
Counting expenses:
- For a single count, use AGG mode + aggregations:[{func:"COUNT", field:"all"}] with no group_by.
- For a count broken down by a dimension, use that dimension as group_by with the same COUNT aggregation.
- receipt_status / memo_status are FILTERS, not group keys. To answer "how many missing receipts", set receipt_status:"RECEIPT_ABSENT" as a filter and aggregate with COUNT (or pair with group_by:["user_first_name","user_last_name"] for a per-user breakdown).
Time bucketing:
- Supported granularities: day, week, month, quarter, year (based on purchased_at).
- Time buckets honor the timezone parameter: bucket boundaries align to the caller's local calendar (defaults to UTC when timezone is omitted), consistent with the tz-aware date range filters.
- For yearly totals across multiple years, group by "year". For a single year's total, omit group_by entirely with a year-bounded purchased_at_start/end and a SUM aggregation.
Naming note: "merchant_category" and "category" are interchangeable group keys — both group on vendor category name. Use either.
DECLINED / FAILED CARD AUTHORIZATIONS ("why was my card declined?"): Failed authorization attempts ARE returned by this tool -- they are expenses whose payment_status is DECLINED (authorization refused) or CANCELED (authorization reversed or expired without clearing). A merchant retrying a charge produces several such records followed by a CLEARED one. To investigate, use LISTING mode with payment_statuses: ["DECLINED", "CANCELED"] plus whatever narrows it down (merchant/vendor, purchased_at_start/end, card, user). Each row's payment_status tells you the outcome, and payment_status_label carries the dashboard's own wording for it -- prefer that over paraphrasing the enum (CREDITED is shown as "Chargeback", and DECLINED is "Declined" on a card but "Failed" elsewhere). Then, for each expense you want explained, call get_expense_by_id with its id to read payment_status_reason -- the actual decline reason (EXCEEDED_CARD_LIMIT, MCC_BLOCKED, CARD_SUSPENDED, SUSPECTED_FRAUD, ...). This tool does NOT return payment_status_reason at all, because resolving it costs one extra lookup per expense; never report "declined for an unknown reason" from a list result.
IMPORTANT
- For large LISTING results (50+ expenses): consider start_expense_download for CSV export instead. Aggregation mode collapses results to one row per group, so it's the right answer when the user wants totals rather than individual transactions.
IMPORTANT
- How to scope expense queries with expense_owner:
🔍 The primary users of this tool are admins and finance operators, so the DEFAULT SCOPE is company-wide. Only narrow to the caller when the user explicitly says so.
IMPORTANT DEFAULT BEHAVIOR: When the query is ambiguous or phrased generically (no possessive "my"/"mine" and no subject "I"), OMIT expense_owner. This returns all expenses across the organization, which is what admins asking questions like "show expenses over $500", "find expenses missing receipts", "which expenses are declined" expect.
⚠️ "show me X" is NOT a personal-scope signal. The "me" is the indirect object of "show", not a claim of ownership. Only the possessive "my"/"mine" or the subject "I" indicates personal scope.
Personal queries (use expense_owner: "ME") apply only when the user explicitly refers to themselves:
- Possessive: "my expenses", "my receipts", "my reimbursements", "my card transactions", "which of my expenses..."
- Subject "I": "how much have I spent", "what did I spend on...", "am I spending more on..."
- Set expense_owner: "ME" only when the user message contains "my", "mine", or "I" as a subject referring to the caller.
- Treat as company-wide: "show expenses", "find expenses", "list expenses", "which expenses", "show me expenses", "expenses over $X", "expenses from [merchant]"
Company-wide queries (OMIT expense_owner — this is the default):
- Any request without a first-person pronoun: "show expenses", "find expenses missing receipts", "which expenses are flagged", "list card expenses over $500"
- Explicit company language: "our burn rate", "we spent", "company expenses", "overall spending", "analyze all card transactions", "total reimbursements"
→ OMIT expense_owner to return all expenses across the organization
Team queries (use expense_owner: "DIRECT_REPORTS" or "ALL_REPORTS"):
- "my team's expenses", "direct reports' spending"
- "expenses from all my reports"
→ Use DIRECT_REPORTS for immediate reports only, or ALL_REPORTS for nested reports
Specific user queries (use user_ids):
- "show John's expenses", "Alice's reimbursements"
→ First call list_users_by_name_or_email to get user ID, then pass user_ids parameter
IMPORTANT
- Location and travel data:
🌍 Location and travel data are ALWAYS null by default unless you explicitly request them via include.
When user asks about WHERE an expense occurred (location, city, country, address):
- Query asks: "Where was that expense?", "What city was this from?", "Show me the location"
→ You MUST pass include: ["location"]
When user asks about travel details (flights, hotels, car rentals):
- Query asks: "Was this a flight?", "Show me travel expenses", "Which hotel?"
→ You MUST pass include: ["travel_metadata"]
When user asks about booking status or trip cancellation (e.g. was a travel booking canceled?): → You MUST pass include: ["travel_booking"]
⚠️ A null location does NOT mean "no location exists" - it means you did not request it in include.
**IMPORTANT
- Bills (BILLPAY) in expenses vs list_bills tool:**
Bills appear in this tool as expenses with type=BILLPAY, but with LESS comprehensive detail than the dedicated list_bills tool.
Use this tool when bills are just ONE expense type in a broader analysis:
- Overall spending: "total expenses this month", "all expenses over $500"
- Mixed queries: "show card and bill expenses", "compare reimbursements to bills"
- General patterns without bill-specific details
Note: Bills here have minimal vendor details and NO due date information
Use list_bills tool when the user needs BILL-SPECIFIC information:
- Due dates, overdue status: "overdue bills", "bills due this week"
- Vendor details: "bills from vendor X", "how much do we owe"
- Payment schedules and comprehensive bill metadata
See list_bills tool description for full bill-specific filtering capabilities.
Expense types: BILLPAY, CARD, CLAWBACK, REIMBURSEMENT
Filter Usage Best Practices:
- For CARD expense queries, always include date filters (purchased_at_start/purchased_at_end) for reliable results
- When using min_amount/max_amount filters, combine with date filters for better performance
Timezone:
- timezone: IANA timezone string (e.g., "America/Los_Angeles", "America/New_York", "Europe/London", "Asia/Tokyo").
Pass the user's timezone from your system context if available. If not provided, defaults to UTC.
IMPORTANT
- Transparency: When presenting results, ALWAYS tell the user which timezone was used for the query:
- If timezone was provided: "Based on your Pacific Time zone, here are expenses from May 6, 2026..."
- If timezone was NOT provided (UTC default): "Note: dates are interpreted in UTC. If you'd like results in your local timezone, let me know your timezone."
Why this matters: When a user says "show this week's expenses", they mean their local week. The timezone parameter ensures date filters match the user's local calendar days. Without it, dates are treated as UTC which may not match the user's intent.
Date Filtering (only one type can be used at a time): IMPORTANT
- Default to purchased_at for ALL time-based queries. The purchased_at filter covers when the transaction actually occurred and is the correct filter for general date queries like "this week", "last month", "recent", "posted this week", etc. Only use posted_at when the user explicitly needs the bank settlement/accounting posting date for reconciliation purposes.
- Purchase Date (DEFAULT): purchased_at_start/purchased_at_end
- Filter by when expense was purchased/transacted. Use this for all general date queries. Pass dates/datetimes in the user's local timezone (will be converted to UTC automatically).
- Posted Date (accounting only): posted_at_start/posted_at_end
- Filter by bank settlement/posting date. Only use when user specifically needs accounting posting dates. Pass dates/datetimes in the user's local timezone (will be converted to UTC automatically).
- Reimbursement Submission Date: reimbursement_submitted_at_start/reimbursement_submitted_at_end
- Filter by when reimbursement was submitted (only works with REIMBURSEMENT type). Pass dates/datetimes in the user's local timezone (will be converted to UTC automatically).
- Assigned Date: assigned_at_start/assigned_at_end
- Filter by when expense was assigned for review. Pass dates/datetimes in the user's local timezone (will be converted to UTC automatically).
Status Filters:
- approval_statuses: Filter by approval/review status. Commonly used: APPROVED, SUBMITTED, OUT_OF_POLICY, CANCELED (the enum also accepts internal states DRAFT, SETTLED, SPLIT, VOID).
- pending_approvals: FROM_ME (pending your approval) or FROM_OTHERS (pending another approval).
- payment_statuses: Payment lifecycle state (NOT_STARTED, SCHEDULED, PROCESSING, CANCELED, CLEARED, DECLINED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT).
- dispute_statuses: Commonly used: DISPUTE_STATUS_IN_PROGRESS, DISPUTE_STATUS_CLOSED (also accepts DISPUTE_STATUS_CANCELLED, DISPUTE_STATUS_COMPLETE).
- reimbursement_export_statuses: EXPORTED or NOT_EXPORTED (reimbursement export state).
- user_status: Filter by spender's user status. Commonly used: ACTIVE, DELETED, DISABLED (also accepts INACTIVE, INVITED, PENDING_ACTIVATION).
Compliance Filters (matches dashboard labels):
- compliance_statuses: Overall compliance status (DOCUMENTATION_DUE, REVIEW_DUE, COMPLETED). Matches the 'Compliance status' filter in the Brex dashboard.
- documentation_statuses: Documentation deadline status (DUE, OVERDUE, COMPLETED). Matches the 'Documentation status' filter in the Brex dashboard. Use [DUE, OVERDUE] to find expenses missing required documentation.
- receipt_status: RECEIPT_PRESENT or RECEIPT_ABSENT.
- memo_status: MEMO_PRESENT or MEMO_ABSENT.
- require_review_reasons: MEALS, CAR_RENTAL, FLIGHTS, LODGING, TRAINS, MILEAGE, MERCHANT_OR_CATEGORY, AMOUNT, OTHERS.
- government_attendees_status: GOVERNMENT_OFFICIAL_PRESENT or GOVERNMENT_OFFICIAL_ABSENT.
Field notes:
- ID filters (department_ids, card_ids, vendor_ids, merchant_ids, merchant_category_ids, expense_category_ids, expense_policy_ids, trip_ids, approver_user_ids, next_approver_user_ids, limit_ids, user_ids) accept IDs only, never names. Resolve names to IDs first with the matching list_* tool (e.g. list_departments, list_vendors, list_merchants, list_merchant_categories, list_expense_categories, list_active_and_upcoming_travel_trips, list_users_by_name_or_email, list_my_limits).
- ERP debit GL account filter (send both parts together): (1) call list_gl_accounts, (2) copy gl_account_field.key into erp_debit_gl_account_field_key, (3) copy one or more accounts[].identifier values (NOT id, NOT value) into erp_debit_gl_account_option_ids. The key is dynamic per accounting integration (shape "custom_gl_account_<uuid>") — never hardcode or guess it. Sending only one of the pair returns a 400.
- group_by dimensions: vendor, vendor_id, category, category_id, merchant_category, merchant_category_id, department, budget, expense_type, expense_status, payment_status, user_first_name, user_last_name, user_id, cost_center. Time buckets: day, week, month, quarter, year. group_by requires at least one aggregation; receipt_status and memo_status are filters, not group keys. Use *_id group keys only when chaining the result back into a filter; otherwise prefer the human-readable name variant.
- aggregations: each entry is { func: SUM | COUNT, field, alias }. SUM is meaningful only on amount fields (billing_amount = USD spend, original_amount = transaction currency, customer_invariant_amount = single comparable currency). COUNT can target any field — use field:"all" for count of expenses. The alias becomes the result-row key.
All dates should be in ISO 8601 format. Results are paginated - use limit and cursor for pagination.