Expensify's Integration Server API is unlike any other REST API: every operation — fetching reports, creating expenses, approving claims — is a single POST to one URL with the operation specified as a JSON string inside the requestJobDescription form parameter. Bubble's API Connector sends this as application/x-www-form-urlencoded (not JSON body). Key gotchas: amounts are in cents ($25.50 = 2550), partnerUserID is usually a 'partner-{string}' identifier (not your email), and a dry-run mode lets you test operations without creating real records.
| Fact | Value |
|---|---|
| Tool | Expensify |
| Category | Finance & Accounting |
| Method | Bubble API Connector |
| Difficulty | Intermediate |
| Time required | 2–4 hours |
| Last updated | July 2026 |
Expensify on Bubble: The Single-Endpoint API Explained
Expensify's Integration Server API is architecturally unique. Unlike every other REST API you have used — where different resources have different endpoints (POST /invoices, GET /customers, DELETE /reports) — Expensify has one URL for everything:
https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations
Every single operation you can perform — fetch expense reports, create individual expenses, approve a submitted report, export data — is a POST to this exact URL. The operation is specified as a JSON string in a form field called requestJobDescription. The body type is application/x-www-form-urlencoded (standard HTML form format), not JSON.
Here is what a 'get reports' request looks like as form data:
partnerUserID=partner-abc123 &partnerUserSecret=your-secret-here &requestJobDescription={"type":"get","credentials":{},"inputSettings":{"type":"reports","filters":{"statusFilters":["Submitted"]}}}
In Bubble's API Connector, you configure one API call with Form body type, two Private parameters for credentials, and one dynamic Text parameter (requestJobDescription) that you populate differently depending on which operation you are running.
This architecture has two important implications for Bubble builders: 1. You build ONE API call in the API Connector, not multiple separate calls for each operation 2. The requestJobDescription parameter must be constructed as a JSON string in Bubble's workflow — using Bubble's 'Format as text' or expression builder to dynamically create the JSON with the right operation type and values
There is also a critically important gotcha about amounts: Expensify stores all monetary values in the minor currency unit. For USD, that means cents. $25.50 is stored as 2550. $125.00 is stored as 12500. Forgetting this means all expenses are 100× the intended amount — a very visible bug that you will catch in testing if you look at report totals carefully.
With those architectural facts clear, the integration becomes manageable: one endpoint, form body, dynamic JSON string, amounts in cents. Let's set it up.
Integration method
Bubble API Connector with a single endpoint (https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations); all operations use Form body type (application/x-www-form-urlencoded) with partnerUserID, partnerUserSecret (both Private), and requestJobDescription as the operation JSON string.
Prerequisites
- An Expensify account with admin access — you need to be an admin on a company policy (Workspace) to access the Integration Server credentials and to create expenses for other employees
- Expensify Integration Server credentials from use.expensify.com/tools/integrations — log in with your admin account, navigate to this URL, and copy the partnerUserID and partnerUserSecret; note that partnerUserID is often 'partner-{string}', not your email
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble → Install)
- A Bubble account — the basic integration works on any plan including free (no Backend Workflows needed for the basic use cases)
- Understanding that each employee who submits expenses through your Bubble app needs their own Expensify account with the same email address you reference in the API calls
- Your Expensify policy ID (policyIDList) — found in your Expensify Workspace Settings URL or in the Integration Server dashboard
Step-by-step guide
Get Credentials from the Expensify Integration Server
Expensify Integration Server credentials are separate from your regular Expensify login — they are special partner API credentials that you generate specifically for integration use. Log into Expensify with your admin account at expensify.com. Then navigate directly to: https://use.expensify.com/tools/integrations On the Integration Server page you will see two values: - partnerUserID — this is often in the format 'partner-XXXXXXXXXXXXXXXX' (a string starting with 'partner-', not your email address). This is the most common source of 'Invalid credentials' errors — builders assume it is their email, copy their email, and get a 400 error. It is not your email. - partnerUserSecret — a long random string, your API password Copy both values. Store them securely — you will add them to Bubble's API Connector as Private parameters. Also note your Expensify policy ID (policyIDList). Find this by going to your Expensify Workspace Settings — the policy ID appears in the URL when you are in the workspace settings page (format: A1B2C3D4E5F6G7H8). You will need this in the requestJobDescription for filtered report queries. If you do not see an Integration Server page, check that you are logged in as an admin user on a company Workspace (not a free personal account). The Integration Server is a business feature. Do NOT share these credentials with employees — they grant admin-level access to your Expensify policy data.
1// Credential types:2// partnerUserID: 'partner-abc123def456ghi789' (NOT your email)3// partnerUserSecret: 'longRandomSecretString123'4// policyID: 'A1B2C3D4E5F6G7H8' (from Workspace Settings URL)56// Where to find policyID:7// Log into Expensify → click your Workspace name → Settings8// URL format: https://www.expensify.com/admin_policies?param={"policyID":"A1B2C3D4E5F6G7H8"}9// Or check Integration Server page — it may list accessible policiesPro tip: If use.expensify.com/tools/integrations shows a blank page or access error, ensure you are logged in as a Workspace admin and that your account has a paid Expensify plan. Integration credentials require a Collect or Control plan — free personal accounts do not include Integration Server access.
Expected result: You have copied the partnerUserID (starting with 'partner-') and partnerUserSecret from the Expensify Integration Server page. You have noted your policyID from the Workspace settings. These three values are ready to configure in Bubble.
Configure the API Connector with Form Body Type
Expensify's API uses application/x-www-form-urlencoded body format — the same format as a standard HTML form submission. This is NOT JSON. Configuring the wrong body type in Bubble's API Connector causes 400 errors even with correct credentials. Go to Plugins tab in your Bubble editor. If API Connector is not installed, click 'Add plugins', search for 'API Connector' (by Bubble), and install it. Click 'Add another API'. Name it 'Expensify'. Set the base URL to: https://integrations.expensify.com Do NOT add any shared headers for authentication — Expensify uses form body parameters for credentials, not HTTP headers. Add a call named 'Run Operation': - Method: POST - Path: /Integration-Server/ExpensifyIntegrations - Body type: Form (this is critical — must be Form, not JSON, not Raw) Add three form parameters: 1. Key: partnerUserID | Value: [partner_user_id] | Enable 'Private' checkbox 2. Key: partnerUserSecret | Value: [partner_user_secret] | Enable 'Private' checkbox 3. Key: requestJobDescription | Value: [request_job_description] | Do NOT mark Private (this value changes per operation and is constructed dynamically in Bubble) For the Initialize Call, you need real values. Temporarily enter your actual partnerUserID and partnerUserSecret as the parameter default values (you can clear them after initialization). For requestJobDescription, use this minimal test payload: {"type":"get","credentials":{},"inputSettings":{"type":"reports","filters":{"policyIDList":["YOUR_POLICY_ID"],"statusFilters":["Submitted","Approved"],"startDate":"2026-01-01"}}} Replace YOUR_POLICY_ID with your actual policy ID. Click 'Initialize call'. A successful response returns a JSON with a 'reports' array. Bubble detects the response fields automatically. IMPORTANT: after the Initialize Call succeeds, clear the hardcoded credential values from the parameter defaults — they should be passed dynamically from your Bubble database at call time, not hardcoded.
1// Bubble API Connector Configuration2{3 "api_name": "Expensify",4 "base_url": "https://integrations.expensify.com",5 "shared_headers": [],6 "call": {7 "name": "Run Operation",8 "method": "POST",9 "path": "/Integration-Server/ExpensifyIntegrations",10 "body_type": "Form",11 "form_parameters": [12 { "key": "partnerUserID", "value": "[partner_user_id]", "private": true },13 { "key": "partnerUserSecret", "value": "[partner_user_secret]", "private": true },14 { "key": "requestJobDescription", "value": "[request_job_description]", "private": false }15 ],16 "use_as": "Action"17 }18}1920// Test requestJobDescription for Initialize Call:21// {"type":"get","credentials":{},"inputSettings":{"type":"reports","filters":{"policyIDList":["YOUR_POLICY_ID"],"statusFilters":["Submitted"],"startDate":"2026-01-01"}}}Pro tip: Set 'Use as' to Action (not Data) for the Expensify call. Even the 'get' operation returns data, but since all operations share one endpoint and the response shape varies, Action mode gives you more flexibility. You can still access response fields using Bubble's 'Result of step X' expressions in subsequent workflow steps.
Expected result: The Initialize Call succeeds and returns an Expensify report list (even if the list is empty — an empty reports array is a valid successful response if no reports match the filter). Bubble detects the response structure. The 'Expensify' API group appears in your API Connector with one 'Run Operation' call.
Fetch Expense Reports in a Bubble Workflow
Now build the workflow that retrieves expense reports from Expensify and displays them in a Bubble Repeating Group. This uses the 'get' type requestJobDescription. The requestJobDescription for fetching reports is a JSON string that Bubble must construct dynamically. The cleanest approach is to define a text constant in Bubble using a 'Format as text' expression in the workflow. Add a button to your page: 'Load Expense Reports'. In the button's workflow: Step 1: Call API → Expensify > Run Operation For the requestJobDescription parameter, click the value field and select 'Create expression'. Build the JSON string using Bubble's text concatenation. Here is the JSON structure you need: {"type":"get","credentials":{},"inputSettings":{"type":"reports","filters":{"policyIDList":["POLICY_ID_HERE"],"statusFilters":["Submitted","Approved"],"startDate":"2026-01-01","endDate":"2026-03-31"}}} In Bubble, build this as a static text string with dynamic parts: - Replace the policyIDList value with a reference to your stored policyID - Replace startDate/endDate with formatted date expressions using 'Current date/time:formatted as text' with format YYYY-MM-DD For the partnerUserID and partnerUserSecret parameters, reference your stored credentials. Best practice: store them in an App Data record (a single 'ExpensifyConfig' record with partnerUserID and partnerUserSecret fields, both marked Private in Privacy Rules). Reference them as 'Search for ExpensifyConfig:first item's partnerUserID'. Step 2: Store results in Bubble's database. The API response contains a reports array. Use 'Make changes to a thing' in a loop — or better: store the raw response and parse it for display. Create an 'ExpensifyReport' data type in Bubble with fields: report_id (Text), employee_email (Text), total_amount (Number), status (Text), submitted_date (Date), report_name (Text). Create ExpensifyReport records from the API response data, setting each field from the corresponding response field. Step 3: Refresh your Repeating Group. Set the Repeating Group's data source to 'Search for ExpensifyReports' to display the stored data. Alternatively, for simpler use cases, set the Repeating Group to 'Get data from external API' → Expensify > Run Operation — this makes a live API call on every page load but avoids storing in the database.
1// requestJobDescription for fetching submitted reports:2// (Pass as Text parameter to Run Operation call)3{4 "type": "get",5 "credentials": {},6 "inputSettings": {7 "type": "reports",8 "filters": {9 "policyIDList": ["A1B2C3D4E5F6G7H8"],10 "statusFilters": ["Submitted", "Approved"],11 "startDate": "2026-01-01",12 "endDate": "2026-03-31",13 "reportIDList": []14 }15 }16}1718// Response shape (partial):19{20 "reports": [21 {22 "reportID": "R12345678",23 "managerEmail": "manager@company.com",24 "accountEmail": "employee@company.com",25 "total": 12750,26 "currency": "USD",27 "status": "SUBMITTED",28 "submitted": "2026-01-15",29 "reportName": "January Business Travel",30 "expenses": []31 }32 ]33}34// Note: total = 12750 means $127.50 (amount in cents)Pro tip: The 'total' field in Expensify's response is in cents (minor currency unit). When displaying report totals in Bubble, divide by 100: 'Report total: [total] / 100 formatted as currency'. Set a Number format in Bubble that shows 2 decimal places. Similarly, when creating expenses in the next step, multiply dollar amounts by 100 before sending.
Expected result: Clicking 'Load Expense Reports' calls the Expensify API and populates your Repeating Group with submitted expense reports. Each row shows the employee email, report name, and total (divided by 100 to show in dollars). Bubble's Workflow Logs confirm a successful 200 response from the Expensify endpoint.
Create Expenses via API
The 'create expense' operation uses the same single Expensify endpoint with a different requestJobDescription. The key concepts: amounts in cents, employeeEmail must match an Expensify account, and a policyIDList must be specified. Build an 'Add Expense' form on your Bubble page with fields: - Merchant name (Text input) - Amount (Number input — the user enters dollars/cents in decimal format) - Date (Date picker) - Category (Dropdown — populated from your Expensify policy's categories) - Business purpose (Text input) In the 'Submit Expense' button workflow: Step 1: Validate inputs — check that amount > 0, merchant is not empty, date is selected. Step 2: Call API → Expensify > Run Operation For requestJobDescription, build the following JSON string dynamically: {"type":"Expense","credentials":{},"inputSettings":{"type":"expenses","employeeEmail":"[current-user-email]","transactionList":[{"created":"[formatted-date]","currency":"USD","amount":[amount-in-cents],"merchant":"[merchant-name]","category":"[category]","comment":"[business-purpose]","transactionID":"[unique-id]"}]}} In Bubble's expression builder: - [current-user-email]: Current User's email - [formatted-date]: Date picker's value formatted as text with format YYYY-MM-DD - [amount-in-cents]: Amount input's value × 100 converted to a whole number (no decimals) - [merchant-name]: Merchant input's value - [category]: Category dropdown's value - [unique-id]: generate with a timestamp + random string (use Bubble's 'generate a random string' expression) Step 3: Show confirmation — display 'Expense submitted successfully!' when the API call returns a success response. Create an ExpenseSubmission record in Bubble's database with the submission details for your records. IMPORTANT: the transactionID field must be unique per expense — Expensify uses it for idempotency. If you submit the same transactionID twice, Expensify creates only one expense. Generate a unique ID per submission using timestamp + random string. Each employee submitting expenses through your Bubble app must have an Expensify account with the same email address as their Bubble account (Current User's email). If an employee's email does not exist in Expensify, the expense creation returns an error about invalid employee.
1// requestJobDescription for creating an expense:2// (Build this as a dynamic Text parameter in Bubble)3{4 "type": "Expense",5 "credentials": {},6 "inputSettings": {7 "type": "expenses",8 "employeeEmail": "employee@company.com",9 "transactionList": [10 {11 "created": "2026-01-09",12 "currency": "USD",13 "amount": 2550,14 "merchant": "Starbucks",15 "category": "Meals & Entertainment",16 "comment": "Client coffee meeting with ABC Corp",17 "transactionID": "bubble-20260109-143022-abc123",18 "billable": false19 }20 ]21 }22}2324// CRITICAL: amount = cents, NOT dollars25// $25.50 → amount: 255026// $125.00 → amount: 125002728// In Bubble expression for amount:29// Amount input's value * 100 :rounded to 0 decimal placesPro tip: Add a dry-run mode for testing. Expensify's API supports dry-run by adding '"dry-run": true' to the requestJobDescription object. Expenses created in dry-run mode are validated but not actually created in Expensify — perfect for testing your JSON construction without creating test expenses that clutter real expense reports. Remove dry-run before going live.
Expected result: Submitting the expense form calls the Expensify API and creates a real expense in the employee's Expensify account under the specified policy. The expense appears in Expensify's web interface under the employee's account within a few seconds. Your Bubble confirmation message shows for the user.
Build the Manager Approval Workflow
The approval operation uses the same single endpoint with 'type': 'update' and an actionList containing 'Approve'. This workflow should only be accessible to managers and should include a confirmation step before sending the approval. Add an 'Approve Report' button to your manager dashboard Repeating Group (visible only when the report status is 'Submitted' — use Bubble's Conditional to hide the button for already-approved reports). In the 'Approve Report' button's workflow: Step 1: Show a confirmation dialog — use Bubble's Alert element or a custom popup: 'Approve report [Report Name] from [Employee Email] for [Total Amount]? This action cannot be undone.' Step 2 (after confirmation): Call API → Expensify > Run Operation For requestJobDescription: {"type":"update","credentials":{},"inputSettings":{"type":"reports","filters":{"reportIDList":["[report_id]"]},"onReceive":{"actionList":["Approve"]}}} In Bubble's expression builder, replace [report_id] with the current Repeating Group cell's ExpensifyReport report_id field. Step 3: Update the local Bubble database — change the ExpensifyReport record's status field to 'Approved' and set an 'approved_by' field to Current User's email and 'approved_at' to Current date/time. Step 4: Refresh the Repeating Group to reflect the status change. For rejections, use the same structure with actionList: ['Reject'] — but note that Reject moves the report back to 'Open' state in Expensify, not 'Rejected'. You may want to add a rejection reason in Bubble's database for audit purposes, even if Expensify does not capture it natively. Security: add a Bubble workflow condition at the start — 'Only when: Current User's role = Manager'. This prevents non-managers from triggering the approval even if they somehow access the button. Also ensure your Privacy Rules restrict the manager dashboard page to manager-role users only.
1// requestJobDescription for approving a report:2{3 "type": "update",4 "credentials": {},5 "inputSettings": {6 "type": "reports",7 "filters": {8 "reportIDList": ["R12345678"]9 },10 "onReceive": {11 "actionList": ["Approve"]12 }13 }14}1516// For rejection:17{18 "type": "update",19 "credentials": {},20 "inputSettings": {21 "type": "reports",22 "filters": {23 "reportIDList": ["R12345678"]24 },25 "onReceive": {26 "actionList": ["Reject"]27 }28 }29}3031// Bubble workflow step after successful approval:32// Make changes to ExpensifyReport (where report_id = R12345678):33// status = 'Approved'34// approved_by = Current User's email35// approved_at = Current date/timePro tip: RapidDev's team has built employee expense portals in Bubble with Expensify integration for HR and finance teams — if your approval workflow needs additional routing logic (multi-level approval, budget threshold checks), book a free scoping call at rapidevelopers.com/contact to design the right architecture.
Expected result: Clicking 'Approve Report' with confirmation triggers the Expensify API update operation. The report status changes to 'Approved' in Expensify's system (visible in the manager's Expensify web interface) and in your Bubble database. The Repeating Group refreshes to hide the Approve button for the now-approved report.
Apply Privacy Rules and Handle Amount Conversion
Before going live, apply Bubble Privacy Rules to protect credential data and expense records, and implement consistent amount conversion throughout your app. Privacy Rules for ExpensifyConfig (your credentials store): Go to Data tab → Privacy → ExpensifyConfig. Add a rule: 'No one can view partnerUserID or partnerUserSecret'. Under 'All Users', uncheck View for both fields. This ensures credentials are only accessed programmatically by Backend Workflows and API calls — no user can read them through Bubble's Data API or the debug panel. Privacy Rules for ExpensifyReport: Add a rule: 'This ExpensifyReport's employee_email = Current User's email' → check View for all fields. This ensures employees only see their own expense reports. For managers, add a second rule: 'Current User's role = Manager AND Current User's managed_team contains this ExpensifyReport's employee_email' → check View for all fields. Amount conversion — implement it consistently in three places: 1. Creating expenses: multiply the user-entered dollar amount by 100 in Bubble's expression builder. Use: 'Amount input's value * 100 :rounded to 0 decimal places'. The rounded-to-zero-places removes any floating point artifacts. 2. Displaying report totals: divide the stored cent value by 100 for display. In your Repeating Group text element: 'ExpensifyReport's total_amount / 100 :formatted as number (2 decimal places)'. Prefix with your currency symbol. 3. Storing in Bubble's database: when you store the total from an Expensify response, you can store it in cents (as-is) or convert to dollars at storage time. Storing in cents avoids floating-point loss and matches Expensify's native format. Just ensure your display always divides by 100. WU economy: the Expensify API integration has low WU cost compared to other integrations — no token refresh, no polling loops, just direct API calls. The main cost driver is if you load expense reports on every page view for many concurrent managers. Cache the report list in Bubble's database and refresh on demand (a 'Refresh' button) rather than on every page load.
1// Privacy Rules for ExpensifyConfig type:2// Add rule: All Users → uncheck View for: partnerUserID, partnerUserSecret3// Purpose: credentials never exposed via Bubble Data API45// Privacy Rules for ExpensifyReport type:6// Rule 1: 'This ExpensifyReport's employee_email is Current User's email'7// → View: all fields (employees see own reports)8// Rule 2: 'Current User's role is Manager'9// → View: all fields (managers see all reports under their policy)1011// Amount conversion examples:12// User enters: $125.5013// Stored / sent to Expensify: 12550 (cents)14// (125.5 * 100 = 12550, rounded to 0 decimal places)1516// Display from Expensify response (total: 12550):17// 12550 / 100 = 125.5 → formatted as '$125.50'1819// Dry-run mode for testing (add to any requestJobDescription):20// { "type": "...", "dry-run": true, ... }21// Validates request without creating real recordsPro tip: Use Expensify's dry-run mode (add 'dry-run': true to requestJobDescription) for the entire development and testing phase. This lets you test your JSON construction, credential setup, and response handling without creating real expenses or approving real reports. Only remove dry-run when you are confident the integration works correctly and are ready for production.
Expected result: Privacy Rules prevent users from seeing other employees' expense reports or accessing API credentials. Amount conversion is consistent: $125.50 is sent to Expensify as 12550 and displayed back as '$125.50'. Dry-run mode returns successful responses for valid requestJobDescription payloads without creating real Expensify records.
Common use cases
Employee Expense Submission Portal
Build a Bubble internal tool where employees submit expense claims — entering merchant name, amount, date, category, and business purpose. The Bubble workflow calls Expensify's API to create the expense under the employee's Expensify account (matched by email). Employees can also view their pending expense reports pulled from Expensify via the 'get reports' operation.
Build a Bubble expense submission page where employees can enter a merchant name, amount, date, and category for a business expense, and submit it to their Expensify account — with a list showing all their pending expense reports below.
Copy this prompt to try it in Bubble
Manager Expense Approval Workflow
Build a Bubble dashboard for managers showing submitted expense reports from their team. Display report totals, line items, and receipt images pulled via Expensify's 'get reports' API call. Provide an 'Approve' button that triggers Expensify's 'update' operation with actionList: ['Approve'], automatically routing approved reports for reimbursement.
Build a Bubble manager dashboard that shows all submitted expense reports from the manager's team, displaying totals and line items, with an Approve button that sends approval to Expensify and triggers reimbursement.
Copy this prompt to try it in Bubble
Finance Team Expense Report Export
Build a Bubble admin tool for the finance team to pull approved expense reports for a date range, display them in a table with employee names, totals, and categories, and export to a spreadsheet. Use Expensify's 'get' type with date range filters and statusFilters: ['Approved'] to retrieve only approved reports ready for accounting reconciliation.
Build a Bubble finance admin tool that retrieves all approved expense reports from the past month from Expensify, displays them grouped by employee with category subtotals, and allows CSV export for the accounting team.
Copy this prompt to try it in Bubble
Troubleshooting
'Invalid credentials' error (400) even though partnerUserID and partnerUserSecret are copied correctly from the Integration Server page
Cause: The most common cause is copying the wrong value for partnerUserID. Expensify's partnerUserID is a 'partner-XXXXXXXX' format string, not your Expensify email address. Builders frequently paste their email address (or their company email) as the partnerUserID, causing credential failures.
Solution: Go back to use.expensify.com/tools/integrations while logged in as an admin. Look specifically at the 'partnerUserID' field — it should start with 'partner-'. Copy exactly that value, not your email address. If the Integration Server page shows your email as the partnerUserID (some account types do this), try using your email as the ID — but the format 'partner-XXXX' is far more common for integration accounts.
API returns 400 error on every call, but credentials are correct and the JSON looks right
Cause: The API Connector body type is set to JSON instead of Form. Expensify's Integration Server requires application/x-www-form-urlencoded format. If Bubble sends a JSON body (with Content-Type: application/json), Expensify cannot parse the partnerUserID and requestJobDescription fields, causing a 400 error regardless of whether the values are correct.
Solution: In Bubble's API Connector, click on your 'Expensify' API group → 'Run Operation' call → check the Body type field. It must be set to 'Form', not 'JSON' or 'Raw text'. If it is set to JSON, change it to Form, re-enter the three parameters (partnerUserID, partnerUserSecret, requestJobDescription), and run the Initialize Call again.
Expense amounts in Expensify appear as 100× the intended value (e.g., $25.50 shows as $2,550)
Cause: The amount was sent in dollars instead of cents. Expensify's API requires all monetary amounts in the minor currency unit (cents for USD). Sending '25.50' as the amount value tells Expensify '2550 cents = $25.50' is wrong — actually '25.50' is interpreted as '25 dollars and 50 cents' but stored as 25 cents = $0.25, or the value 2550 is used directly as $2550 depending on interpretation.
Solution: Multiply all dollar amounts by 100 before sending to Expensify. In Bubble's expression builder for the amount field in requestJobDescription: 'Amount input's value * 100 :rounded to 0 decimal places'. This converts $25.50 → 2550 and $125.00 → 12500. Verify in Expensify's web interface that the created expense shows the correct dollar value after this fix.
requestJobDescription causes a JSON parse error or 400 even though the JSON looks valid
Cause: Building JSON strings in Bubble's expression builder using text concatenation can introduce escaping issues, extra whitespace, or mismatched quotes if expressions are constructed incorrectly. Single quotes inside string values, or line breaks in the expression, can break JSON validity.
Solution: Use Bubble's 'Format as text' with a template that substitutes only the dynamic values (email, date, amount) — keep static parts of the JSON as fixed strings and use :<substitution> for dynamic parts. After building the expression, test it by temporarily assigning the constructed JSON to a text field in a test record and reading it back — verify the stored text is valid JSON. You can also paste the value into a JSON validator (jsonlint.com) during testing to confirm validity.
Expense creation succeeds but the expense does not appear in the employee's Expensify account
Cause: The employeeEmail in the requestJobDescription does not match an existing Expensify account. Expensify silently accepts the API call but creates the expense against a non-existent or mismatched account, making it invisible in the web interface.
Solution: Verify that every employee who submits expenses through your Bubble app has an active Expensify account with exactly the same email address as their Bubble account (Current User's email). Check that the email is spelled correctly and uses the same domain. In Expensify's web interface, check People → Members under your Workspace policy to see all registered employees. If an employee is not listed, they need to be invited to the Workspace before you can create expenses for them via the API.
Best practices
- Always use Expensify's dry-run mode ('dry-run': true in requestJobDescription) during development and testing — this validates your API requests without creating real expense records or triggering real approvals, making it safe to test extensively without cluttering production data.
- Store partnerUserID and partnerUserSecret in a Bubble App Data record with Privacy Rules that prevent any user from reading them — use Private checkbox in API Connector AND restrict the data type fields in Privacy settings for defense in depth.
- Convert monetary amounts to cents immediately at the input level — multiply by 100 and round to 0 decimal places before building the requestJobDescription JSON, and convert back by dividing by 100 only for display purposes.
- Always include a transactionID in expense creation calls and generate it as a unique value (timestamp + random string) — Expensify uses transactionID for idempotency; if a workflow runs twice (e.g., network retry), a unique ID prevents duplicate expenses.
- Add a manager confirmation dialog before calling the 'Approve' operation — approvals can trigger ACH reimbursement and cannot be easily undone; the extra click prevents accidental approvals from UI interactions.
- Apply Bubble Privacy Rules to your expense data types so employees can only see their own reports — without Privacy Rules, the Bubble Data API exposes all expense records to any authenticated user.
- Cache expense report lists in Bubble's database and refresh on user action rather than live-loading from Expensify on every page load — Expensify's API has undocumented rate limits and Bubble WU costs accumulate quickly on high-traffic pages.
- Confirm each employee has an active Expensify Workspace membership before enabling expense submission in your Bubble app — creating expenses for employees who are not Workspace members silently fails or creates orphaned records.
Alternatives
Dext is designed for the accountant side of expense management — receiving and OCR-processing receipts and invoices from clients for ledger entry. Expensify is designed for the employee side — submitting expense claims for reimbursement with manager approval workflows. If your Bubble app is for employees claiming business expenses, use Expensify. If it is for bookkeepers or accounting firms processing client documents, use Dext.
QuickBooks is an accounting ledger, not an expense management tool. However, Expensify natively syncs approved expenses to QuickBooks Online, so many Bubble apps use both: Expensify for the expense submission and approval workflow, QuickBooks for the accounting entries. If you only need to record expenses in an accounting system without a formal submission and approval flow, connecting directly to QuickBooks may be simpler.
Stripe is for processing payments from customers, not for managing employee expenses. Choose Stripe when you are building a product that charges customers; choose Expensify when you are building an internal tool that tracks and reimburses employee business expenses. They solve completely different problems and are sometimes used in the same app.
Frequently asked questions
Why does Expensify use a single endpoint for all operations instead of separate REST endpoints?
Expensify's Integration Server was built as a job-execution system — you describe a job (operation type, filters, actions) and the server processes it. This design predates modern REST conventions. In Bubble, it means you configure one API call and change the requestJobDescription parameter to switch between operations like 'get reports', 'create expense', and 'approve report'. It is unusual but not difficult once you understand the pattern.
Where do I find my partnerUserID? It does not look like my email address.
Your partnerUserID is at use.expensify.com/tools/integrations — navigate there while logged into your admin Expensify account. The partnerUserID is usually in the format 'partner-XXXXXXXXXXXXXXXX' (a string starting with 'partner-'), not your email address. This is the most common source of 'Invalid credentials' errors — make sure you copy the value labeled 'partnerUserID' on that page, not your email or any other identifier.
Does each employee need their own Expensify account?
Yes. When you create expenses via the API, you specify an employeeEmail in the requestJobDescription. This must be the email of an Expensify account that is a member of your Workspace. Each employee who submits expenses through your Bubble app must have an active Expensify account under the same email they use in Bubble (Current User's email). If they are not already in Expensify, you will need to invite them to your Workspace.
Why must amounts be in cents? My amount inputs are in dollars.
Expensify stores all monetary amounts in the minor currency unit — cents for USD, pence for GBP, etc. This is a standard practice in payment and expense APIs (Stripe does the same). The API never accepts decimal dollar amounts. In Bubble, multiply user-entered dollar amounts by 100 and round to 0 decimal places before including in the requestJobDescription. Divide by 100 when displaying Expensify amounts back in your UI.
Can I use dry-run mode to test without creating real expenses?
Yes — and you should. Add '"dry-run": true' to any requestJobDescription object and Expensify validates the request and returns a success response without actually creating the expense or performing the approval. This is invaluable during development. Remove 'dry-run': true only when you are ready to go live. Note that dry-run is not documented on all Expensify plan tiers — confirm it works with your credentials before relying on it.
What is a policyIDList and where do I find my policy ID?
A policy in Expensify is a Workspace — a group of employees with shared expense rules, categories, and approvers. The policyIDList filter in the requestJobDescription restricts results to expenses under a specific Workspace. Find your policy ID in the Expensify web interface: go to Settings → Workspaces → click your Workspace → the URL contains your policy ID in the format 'policyID=A1B2C3D4E5F6G7H8'. You can also find it on the Integration Server page at use.expensify.com/tools/integrations.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation