Expensify
Ask questions about your Expensify account and get answers from your real data — not guesses. Connect your Expensify account and ask natural language questions about your finances. ChatGPT will query your actual account to answer them. What you can ask: - "How much did I spend on travel last quarter?" - "Which team members have unsubmitted expense reports?" - "Show me all approved expenses that haven't been reimbursed yet." - "Find my receipts from Uber over the past 30 days." - "Which categories am I spending the most on this month?" Read-only. This plugin never creates, modifies, or deletes anything in your account.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Primary Subcategory
- Corporate Spend, Expense & Procurement
- Secondary Subcategories
- None listed
- Brand
- Expensify
- Access
- Account required
- First tracked
- 2026-07-28
- Tool count
- 3
- Geography
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is visible.
ChatGPT Plugin Discoverability Score
ChatGPT organic discovery is not live yet
Expensify is tracked in the ChatGPT Plugin registry. Public organic-discovery measurement is not live for ChatGPT yet, so there is no score to publish today.
Get notified when your score goes live
Enter your work email and we’ll notify you when ChatGPT Plugin organic discovery scoring launches.
No spam. Unsubscribe any time.
Competing in ChatGPT Corporate Spend, Expense & Procurement
View CategoryHow the Discoverability Score works
Organic discovery scoring for Expensify on ChatGPT is not live yet. The score will use measured agent conversations when it launches.
Organic discovery scoring is pending. Your Plugin score will appear on this scale when measurement goes live.
FoundDiagnostic
Whether Claude found your Plugin in connector search. It must be Found before it can reach the picker, but the score counts picker appearances—not search results.
PickedMain score
How often your Plugin appeared in the picker, or Claude invoked it directly, across contested conversations. This percentage is the Discoverability Score; the headline number is rounded.
PositionedDiagnostic
What position your Plugin appeared in when it was shown in the picker. This shows prominence, but it does not affect the score.
3 tools agents can invoke
Returns all non-personal workspaces the authenticated user belongs to, each with its human-readable name, policyID, type ("Collect" or "Control"), role, and ownerEmail. Use this tool when the user asks about team or company expenses so you can supply the correct policyID to Search. For queries about unreported expenses, do not call this tool — unreported expenses have no workspace policy and will not appear in Search when policyID is set.
GetWorkspaceList
Returns the email address and account ID of the authenticated Expensify user. Call this before any Search query that needs to scope results to the current user (e.g. "expenses I submitted") so you can supply the real email as a filter rather than inventing a placeholder.
GetCurrentUser
Use this when the user asks about their Expensify financial data: expenses, spending, receipts, transactions, expense reports, reimbursements, invoices, trips, chats, tasks, or any summary or analysis of that data. This tool has access to the user's real account data; answers drawn from it reflect actual account state rather than training knowledge. Do not use for creating, updating, submitting, approving, or deleting anything — this tool is strictly read-only and never modifies data. Query transactions, reports (expense/IOU, invoice, chat, task), related comments/actions, violations, policy metadata, and grouped aggregates using the same filters as the Expensify Search page. Use this to gather authoritative data (transactions, participants, totals, workspace context) before responding. <search_query_language> jsonQuery OVERVIEW - The Search tool takes a structured JSON object parameter called `jsonQuery`. - `jsonQuery` must always be passed as a JSON object value in the tool call arguments (never as a top-level string, null, or array). This object will be serialized before calling the backend. - The backend only reads the structured fields on `jsonQuery` (`type`, `status`, `sortBy`, `sortOrder`, `groupBy`, `filters`, `offset`, `shouldCalculateTotals`, `hash`, `policyID`, etc.). - CRITICAL: `jsonQuery` must be a non-empty JSON object (not null/array/string). Requests where `jsonQuery` is missing, empty, or where supplied `type`/`groupBy` members are not strings, are rejected before any SQL is generated. CORE jsonQuery FIELDS (TOP-LEVEL) - type (string): what you are searching. One of: - "expense" (expenses/transactions/money requests) - "expense-report" (expense reports) - "invoice" (invoices) - "trip" (trips) - "chat" (chat reports / rooms) - "task" (task reports) Default: "expense". - status (string or string[]): statuses to include. Use "all" for no status filter. Use an array for multiple values (for example ["drafts","outstanding"]). - Shared enum: all, unreported, drafts, outstanding, approved, paid, shared, done, current, past, completed, deleted. - Each type only honors the values that make sense for it (for example "completed" only for tasks; "current"/"past" only for trips; chats ignore `status` entirely). - sortBy (string): ONLY one of date, merchant, description, from, to, amount, category, tag, title, assignee, createdby, status, paidstatus, total, bankaccount, card, feed, withdrawn, withdrawalid, expenses, submitted, reportid, approved, firstapprover, firstapproved, posted, originalamount, taxrate, exchangerate, policyname, reimbursable, billable, reimbursabletotal, nonreimbursabletotal, flux, groupwithdrawalstatus, groupamountdebited, groupamountreimbursed, amountdebited, amountreimbursed, attendees, totalperattendee, taxcode, mcc, glcode, tagglcode, inserted. - sortOrder (string): "asc" or "desc". - groupBy (string, optional): "from", "card", "withdrawal-id", "category", "merchant", "tag", "month", "week", "year", "quarter" (only valid for type:"expense", type:"invoice", type:"trip"). See FLUX ANALYSIS section for how to include flux analysis fields with groupBy responses. If you run a flux analysis query, you don't need to run the same grouped-query without flux analysis. Adding fluxPeriod to the query will return the same data, just including the flux fields. - policyID (string, optional): scopes the search to a single workspace. Set this from `<CURRENT_REPORT_CONTEXT>` (look for "Workspace ID (policyID): ..."). Omit `policyID` only when the user has explicitly asked for a cross-workspace search; otherwise default to the workspace from `<CURRENT_REPORT_CONTEXT>`. - offset (integer): number of rows to skip for pagination. The backend echoes this in `search.offset` and outputs `nextOffset` as the suggested value for the next page. - shouldCalculateTotals (boolean): when true and `type` is "expense" or "expense-report", the backend also returns aggregated `total`, `count`, `currency` in `search`. - hash (integer): arbitrary client-supplied hash used to key the snapshot; echoed back unchanged. - searchKey (string, optional): freeform key used by the client for differentiating search contexts. FILTER FIELD: jsonQuery.filters - `filters` is required on every jsonQuery. Pass JSON null when the query needs no scoping filters (never pass an empty object `{}`). - When scoping is needed, `filters` is a JSON expression tree that encodes all non-root filters (date ranges, amounts, merchants, categories, tags, card IDs, etc.). - Conceptually each leaf in this tree corresponds to a condition of the form: - key op value (for example date>="2025-07-01", amount>"10000", merchant:"Whole Foods") - The tree is built from objects of the form: - { "operator": "<op>", "left": "<key>", "right": "<value or array of values>" } where <op> is one of "eq", "neq", "gt", "lt", "gte", "lte" - or logical nodes: { "operator": "and" | "or", "left": <subnode>, "right": <subnode> } - Common keys (always strings) for `left` include: - date, submitted, approved, paid, exported, posted, withdrawn - amount, amountDebited, amountReimbursed, total, currency, groupCurrency, purchaseAmount, purchaseCurrency. amount/total are always provided in cents, so $100.00 needs to be provided as 10000. - reimbursable, billable - from, to, payer, exporter, assignee, attendee, createdBy - reportID, withdrawalID, orderDealNumbers - category, tag, expenseType, withdrawalType - merchant, description, title, keyword, has, receiptType, is, in, submitterUserID, submitterPayrollID - cardID, feed, taxRate - reportField-* variants for custom fields - NOTE: During the migration, `status` and `policyID` are still top-level jsonQuery fields for AgentZero. - NOTE: The merchant filter also supports the "contains" operator for partial matching (e.g., { "operator": "contains", "left": "merchant", "right": "Amazon" } matches "Amazon", "Amazon Fresh", etc.). Use "eq" for exact match and "contains" when the user's intent is a substring/partial match. - Values for `right` are strings or arrays of strings; multiple values correspond to OR within that key (for example reportID:1111,2222). SPECIAL FILTER VALUES Search recognises a fixed set of special `right` values for "missing X" / "never X" / presence / boolean queries. Always use these for those queries — never pass `""` (empty string). Empty-string behavior varies by filter and is never what you want: for text filters (title, description, tag) it widens to a wildcard match that returns every non-empty row, and for other filters it silently misses rows stored as NULL or with an explicit sentinel value. - "Missing X" / "no X" / "uncategorized" → pass `right:"none"` (lowercase). Recognised for these `left` keys: category, description, submitterPayrollID, submitterUserID, tag, title. The `category` key also accepts `right:"Uncategorized"` as a synonym — missing-category rows are stored as either empty OR the literal `"Uncategorized"`, so both forms match both storage variants. This synonym is `category`-only. - "Never X" / no event yet → pass `right:"never"` (lowercase). Recognised for these `left` keys: submitted, approved, paid, exported. - Boolean flags → pass `right:"yes"` or `right:"no"` for these `left` keys: reimbursable, billable. Do NOT pass `true`/`false`, `"true"`/`"false"`, or `"1"`/`"0"`. - Presence/absence of receipt, attachment, tag, category, rejection, or a submit-time violation → use `left:"has"` with `right` set to one of: "receipt", "attachment", "tag", "category", "submitted-violation", "rejection". For "WITHOUT" use the same filter with `operator:"neq"`. There is no separate `receipt` or `attachment` filter key; their presence is only addressable via `has`. `submitted-violation` matches transactions that were flagged in the violation snapshot recorded when their report was submitted, submitted-and-closed, or closed, even if the transaction has since moved to another report, so use it for "expenses submitted with violations" style queries. FLUX ANALYSIS (additional period-over-period comparison fields for grouped results) - Flux analysis is only available when `type:"expense"` and `groupBy` is present. - Enable flux analysis by adding a `fluxPeriod` leaf node anywhere under `jsonQuery.filters`: - { "operator": "eq", "left": "fluxPeriod", "right": "month" | "quarter" | "year" } - When `fluxPeriod` is present: - A date filter is required in the `jsonQuery.filters` tree. - When fluxPeriod is provided, `sortBy` values MUST be limited to: - "flux", "total", "grouptotal", "expenses", "groupexpenses" - `sortBy:"flux"` sorts by `fluxPercent` (DESC by default) and pushes NULL `fluxPercent` to the end. - `fluxPeriod` is a meta-instruction (it does not directly filter rows); it instructs Search to compare the grouped totals in the requested date range against the prior-period totals shifted by the requested fluxPeriod and attach flux fields to each group. - Flux fields are computed from the current-period group total and the prior-period (basis) group total: - fluxBasis = prior-period group total - fluxAmount = (current total - fluxBasis) - fluxPercent = ((current - fluxBasis) / ABS(fluxBasis)) * 100 - Missing/zero prior-period behavior (what you will actually see in `data[groupKey]`): - If the group has **no matching rows in the prior period OR a prior-period total of 0**: - `fluxBasis` is present and equals 0 - `fluxAmount` is present and equals the current group's `total` - `fluxPercent` is absent (it would be division-by-zero / infinite growth) - This group is "new this period" relative to the comparison window; include it when the user asks for relative increases (e.g. "more than doubled"). - If the group has **no matching rows in the current period** (but did in the prior): - `total` is 0 - `count` is 0 - `fluxBasis` is present (prior-period total) - `fluxAmount` is present (= -fluxBasis) - `fluxPercent` is present and equals -100.0 - NOTE that if you run a grouped query with flux analysis, you don't need to run an the same grouped-query without flux analysis. Adding fluxPeriod to the query will return the same data, just including the flux fields. CRITICAL: TYPE-SPECIFIC BEHAVIOR AND GOTCHAS - status:"all" is treated as “no status filter” (empty status). - Some statuses only matter for certain types ("completed" for tasks; "current"/"past" for trips); others are ignored for incompatible types. - type:"chat" ignores `status`; use state flags under filters instead (for example is:unread -> filter on an "is" field). - CROSS-CHAT LOOKUP: to read a DIFFERENT chat the user references by reportID (or by a report URL/permalink like https://new.expensify.com/r/<reportID>), run type:"chat" with a `reportID` filter (e.g. { "operator": "eq", "left": "reportID", "right": "<reportID>" }). The returned reportActions_<reportID> messages can then be used as context for the current turn. Results are ALWAYS scoped to reports the requesting user can already access and are never elevated — if nothing comes back, the user cannot access that report, so say so instead of guessing. When filtering by reportID the default 3-month recency window does not apply, so the full visible history is returned. - type, keyword, groupCurrency, and groupBy are not negatable in practice: your filters should not try to invert them. - groupBy is only valid for type:"expense", type:"invoice", type:"trip"; using it with other types is an invalid query. - When groupBy is present, the backend chooses a sort key tied to the group (employee/card/withdrawal). `sortBy:"total"` will NOT change the group order; if you need "Top N by total", sort the grouped results yourself using the group totals you get back. - Do not rely on the ordering of keys inside the `data` object for group ordering. Use `orderedKeys` (documented below). - shouldCalculateTotals:true is only honored for type:"expense" and type:"expense-report". - If groupCurrency is invalid or missing, the backend falls back to the active policy currency. - `sortBy:"total"` only applies when the query returns aggregated rows (e.g. when groupBy is present or when searching for type:"expense-report"). When returning plain expenses, `sortBy:"total"` is ignored and you should use `sortBy:"amount"` if requested. EXAMPLE jsonQuery (unscoped — no filter tree) { "type": "expense", "status": "all", "filters": null } EXAMPLE jsonQuery (grouped flux analysis) { "type": "expense", "status": ["drafts", "outstanding"], "sortBy": "flux", "sortOrder": "desc", "groupBy": "from", "offset": 0, "shouldCalculateTotals": true, "policyID": "AB36C09FE22E8B58", "hash": 1, "filters": { "operator": "and", "left": { "operator": "and", "left": { "operator": "gte", "left": "date", "right": "2025-07-01" }, "right": { "operator": "lt", "left": "date", "right": "2025-10-01" } }, "right": { "operator": "eq", "left": "fluxPeriod", "right": "month" } } } </search_query_language> <search_result_format> The response is a single Onyx snapshot with `data` (what you analyze) and `search` (metadata). When `groupBy` is used, it also includes `orderedKeys` (group ordering). `data` fan-outs: - transactions_<transactionID>: - `transactionID`: unique expense ID - `reportID`: ID of the expense report this transaction belongs to - `policyID`: workspace ID - `created`: expense date - `modifiedCreated`: user-edited date override; use instead of `created` when non-empty - `merchant`: business or store name — ALWAYS use as the display name when listing expenses; never substitute `comment` for this field - `modifiedMerchant`: user-edited merchant override; use instead of `merchant` when non-empty - `amount`: amount in cents (e.g. 10050 = $100.50) - `modifiedAmount`: user-edited amount override; use instead of `amount` when non-empty - `currency`: ISO 4217 code (e.g. "USD", "GBP") - `modifiedCurrency`: user-edited currency override - `category`: expense category - `tag`: expense tag (project, department, or cost center) - `groupAmount`: amount converted to the search's target currency (cents); present when `groupCurrency` is set - `groupCurrency`: target currency used for `groupAmount` - `groupExchangeRate`: exchange rate used to compute `groupAmount` - `comment`: object with a single key `comment` containing the user-written text description of the expense — supplementary context only, NOT the merchant name - `transactionThreadURL`: pre-built URL to drill into this expense (e.g. "https://new.expensify.com/r/12345"). Wrap the merchant in `<a href="{{transactionThreadURL}}">…</a>` when rendering the transaction. Use VERBATIM; never construct this URL yourself. - `receipt`: present only when the expense has a receipt. Object with `source` (a shareable link to the receipt image/file) and `filename`. Use `receipt.source` VERBATIM when the user asks for the receipt; never construct a receipt URL yourself. Absent when the expense has no receipt. - report_<reportID>: expense/IOU, invoice, chat (DM/group/workspace), and task reports: - `reportID`, `reportName`, `type`: report identity and type ("expense", "iou", "invoice", "chat", "task") - `chatType`: chat subtype (e.g. "policyExpenseChat", "invoice") - `chatReportID`: parent chat report ID (for money requests and linked invoices) - `ownerAccountID`: accountID of the report submitter/owner - `managerID`: accountID of the approver - `policyID`: workspace ID - `stateNum`, `statusNum`: numeric report state and status - `total`, `reimbursableTotal`, `nonReimbursableTotal`, `unheldTotal`: report totals in cents - `currency`: report currency - `created`, `submitted`, `approved`: key dates - `transactionCount`: number of transactions on this report - `isWaitingOnBankAccount`: payment pending a bank account - `isExportedToIntegration`: whether exported to an accounting integration - `parentReportID`, `parentReportActionID`: parent linkage - `participants`: participant list (when populated) - `invoiceReceiver`: receiver details (invoice reports only) - `nextStep`: next required action (expense reports only) - `oldPolicyName`: previous workspace name if renamed - reportActions_<reportID>: present for type:"chat" searches; an object keyed by reportActionID holding the visible chat messages for that report. Use this to read what was said in a chat (including a chat referenced by reportID — see CROSS-CHAT LOOKUP below). Each reportAction has: - `reportID`: the report the message belongs to - `reportActionID`: unique message ID - `created`: message timestamp - `accountID` / `actorAccountID`: accountID of the message author (join with personalDetailsList for name) - `actionName`: the action type (e.g. "ADDCOMMENT") - `message`: array of message fragments; use the `html`/`text` of each fragment for the message content - personalDetailsList: - `accountID`, `displayName`, `firstName`, `lastName`, `email`, `login` - group_<key>: aggregated totals/counts per group (employee, card, category, month, etc.) when `groupBy` is used. Each group object includes: - `total`: the group's summed amount in cents (e.g. 485000 = $4,850.00), same unit as the per-row `amount` field — a five- or six-digit `total` is a few hundred to a few thousand dollars, not hundreds of thousands - `count`: the number of transactions in the group - `currency`: ISO 4217 code for the group's amounts (e.g. "USD") CROSS-BORDER CONVERSION FIELDS - A payment that converted currency reports both sides of the conversion, each in its own currency: - `debitedAmount` (integer, cents) and `debitedCurrency`: what the company was debited. - `creditedAmount` (integer, cents) and `creditedCurrency`: what the employee was reimbursed. - The two currencies differ, so never add or compare the two amounts directly, and never assume either matches the row's own `currency`. - They appear on `group_<key>` objects under groupBy:"withdrawal-id", describing that settlement, and on `report_<reportID>` objects, describing the report's newest payment that moved money. Cancelled payments and payments held for monthly settlement are ignored there, so a report can report an older payment's amounts. - Both sides are omitted when the payment stayed in one currency, and one side is omitted on its own when only that amount was recorded. Their absence says nothing about whether the payment succeeded. - `amountDebited` and `amountReimbursed` filter on these amounts, in cents, taking the same operators as `total`. They match against the same payment the `report_<reportID>` fields describe, and compare amounts as reported, without converting between currencies. They only apply to type:"expense-report", since a payment pays a whole report and no single expense has an amount of its own. A report whose payment stayed in one currency matches neither filter, and so does a report that was never paid. - `sortBy:"groupamountdebited"` and `sortBy:"groupamountreimbursed"` rank groups by these amounts as reported, without converting between currencies, so a group in one currency can rank above a group worth more in another, and groups with no amount sort first ascending and last descending. They only apply to groupBy:"withdrawal-id". Every other grouping keeps its own ordering instead (groupBy:"category" stays in category order, and so on), and an ungrouped query comes back in date order. Read the `sortBy` in the response to see which order you were given. - `sortBy:"amountdebited"` and `sortBy:"amountreimbursed"` rank reports the same way, on the payment the `report_<reportID>` fields describe. They only apply to type:"expense-report". Any other ungrouped query comes back in date order, and a grouped one keeps its grouping's ordering. Read the `sortBy` in the response to see which order you were given. GROUPED RESULTS ORDERING (`orderedKeys`) - When `groupBy` is used, the backend returns an additional top-level array `orderedKeys` (siblings to `data` and `search`). - `orderedKeys` contains the `group_<key>` group keys in the backend's effective ordering (i.e. the SQL order after applying sortBy/sortOrder and any fallbacks). Use this to iterate groups in sorted order; do not rely on JSON object key ordering. GROUPED PERCENT-OF-TOTAL FIELD (present on each group object when `shouldCalculateTotals` is enabled) - `percentOfTotal` (float): the percent of the overall grouped total represented by this group, i.e. (total / SUM(total)) * 100. - Present only when `shouldCalculateTotals: true` for grouped expense searches (e.g. `type: expense` with `groupBy`). - Omitted when the overall total is 0 (to avoid division by zero). FLUX ANALYSIS FIELDS (present on each group object when `fluxPeriod` is enabled) - `fluxBasis` (integer, cents): the prior period total for this same group key (e.g. last month / last quarter / last year). Always present; equals 0 when the group has no prior-period rows. - `fluxAmount` (integer, cents): the change vs. the prior period, i.e. (current total - fluxBasis). Always present; equals the current group's `total` when `fluxBasis` is 0. - `fluxPercent` (float): percent change vs. the prior period, i.e. ((current - fluxBasis) / ABS(fluxBasis)) * 100. - Omitted when `fluxBasis` is 0 (division-by-zero / infinite growth). A group with current-period spend but no `fluxPercent` had no spend in the comparison period; include it when filtering for relative increases. `search` metadata: - Includes: type, status (string), offset, hasMoreResults, hasResults, nextOffset, searchURL. - When shouldCalculateTotals is true (and type is expense or expense-report) and at least one row is returned, also includes: total, count, currency (values are taken from the first row of the current page). `total` is in cents (e.g. 485000 = $4,850.00), same unit as the per-row `amount` field. - `violationSnapshotStartedAt` (date string, YYYY-MM-DD): present only when the query filters include `has` with the value `submitted-violation`; always `2026-06-20`, the first complete day covered by submit-time violation snapshots. Earlier dates lack complete snapshot coverage, so an empty result does not establish that no submitted violations existed. - hasResults flips to true if the current page has rows **or** if Search detects matching rows elsewhere (via `getHasResults`), so you may see hasResults:true even when a specific page is empty. - You primarily need nextOffset and hasMoreResults for pagination. - If hasMoreResults is true, it indicates that there are more results to fetch for this search query. - nextOffset indicates the offset to use for the next search query. - If you call the search tool again with the same JSON query and the provided nextOffset, you will be able to access more results (until hasMoreResults becomes false). - searchURL is a pre-built, frontend-compatible URL (e.g. "https://new.expensify.com/search?q=type%3Aexpense%20status%3Aall...") that you MUST use when linking to search results in your response. Do not construct URLs manually; always use this field. Note: backend-only features like fluxPeriod are automatically excluded from this URL. important notes: - any amounts (including those for fluxBasis, fluxAmount, totals, etc.) are always stored in the database in cents, so $100.00 needs to be provided as 10000, and any results for those fields are always returned in cents. Always extract insights from the `data` collections; never guess. Use `search` only for pagination or summary (nextOffset, hasMoreResults). </search_result_format>
Search
How do I improve a ChatGPT Plugin's discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
What are Expensify alternatives on ChatGPT?
As of 2026-08-14, Expensify competes with Allowance, Brex, Perk, Ramp, Ramp Data, Tropic in ChatGPT Corporate Spend, Expense & Procurement, ranked by public Discoverability Score.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.