Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Expensify

To connect Expensify with Bolt.new, prompt Bolt to create an API route that calls Expensify's Integration Server API using your Expensify partnerUserID and partnerUserSecret from the .env file. Expensify's API uses a unique command-based request format — POST with JSON containing type, credentials, and command fields. Outbound API calls work in Bolt's WebContainer during development. Deploy to Netlify or Vercel before testing any webhook-style notifications from Expensify.

What you'll learn

  • How to get your Expensify Integration Server API credentials (partnerUserID and partnerUserSecret)
  • How to structure Expensify API requests using the command-based JSON format
  • How to create an expense dashboard in Bolt that displays pending and approved reports
  • How to create and submit expense reports programmatically via API routes
  • How to handle Expensify API authentication errors and rate limit responses
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read20 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

To connect Expensify with Bolt.new, prompt Bolt to create an API route that calls Expensify's Integration Server API using your Expensify partnerUserID and partnerUserSecret from the .env file. Expensify's API uses a unique command-based request format — POST with JSON containing type, credentials, and command fields. Outbound API calls work in Bolt's WebContainer during development. Deploy to Netlify or Vercel before testing any webhook-style notifications from Expensify.

Build an Expense Management Dashboard in Bolt.new with Expensify's API

Expensify dominates the expense management market with a powerful combination of mobile receipt scanning, automated policy enforcement, and direct accounting integrations. Their Integration Server API, while less well-known than consumer-facing REST APIs, provides comprehensive programmatic access to the same expense creation, report management, and approval workflows available in the Expensify web application. The API is used by businesses to integrate Expensify with their own ERP systems, HR platforms, and custom internal tools.

Expensify's API architecture differs from typical REST APIs in a notable way: instead of multiple endpoints for different resources, all API calls go to a single endpoint (https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations) via HTTP POST. The request body is a JSON object with a type field (usually 'create', 'get', or 'update'), a credentials object with your partnerUserID and partnerUserSecret, and a command string that specifies the exact operation. This command-based pattern means Bolt's generated code always posts to the same URL with different command payloads, which can feel different from REST but is actually very consistent once you understand the pattern.

Building an expense dashboard in Bolt.new means creating a UI that shows your team's pending and approved expense reports, total spend by category, and reimbursement status — all pulled from Expensify's API through server-side routes. Since Expensify API access requires a subscription, ensure your account tier includes API access before building. The Pro tier and above include Integration Server API access. During development in Bolt's WebContainer, outbound API calls to Expensify work directly — the WebContainer supports any outbound HTTP request.

Integration method

Bolt Chat + API Route

Bolt.new integrates with Expensify through server-side API routes that call Expensify's Integration Server API. The Integration Server uses an unusual command-based request format: all requests are POST to a single endpoint with a JSON body containing type, credentials (partnerUserID and partnerUserSecret), and a command string. Your credentials live in the .env file and the API route translates your app's actions into Expensify commands. Expensify API access requires an active Expensify subscription.

Prerequisites

  • An active Expensify account on Pro tier or above (Integration Server API access requires a paid Expensify subscription)
  • Your Expensify Integration Server credentials: partnerUserID and partnerUserSecret from Expensify Settings → Integrations → API
  • A Bolt.new project configured with Next.js (for API routes) or Vite with a deployment target that supports serverless functions
  • Basic understanding of Expensify's data model: policies, reports, and transactions
  • A deployed URL on Netlify or Vercel if your integration requires any incoming notifications from Expensify

Step-by-step guide

1

Get Your Expensify Integration Server Credentials

Expensify Integration Server credentials are different from your standard Expensify login. They are separate API keys specifically for programmatic access and are found in a dedicated section of Expensify's settings. Log in to your Expensify account at expensify.com and navigate to Settings (gear icon in top navigation) → Integrations → API. If you do not see an Integrations section in Settings, your account tier may not include API access — Integration Server access is available on Professional ($9/user/month) and Control ($18/user/month) plans. On the API settings page, you will find your partnerUserID (an email-format identifier unique to your integration) and a partnerUserSecret (a long alphanumeric string that acts as your API password). These credentials authenticate all requests to the Integration Server — every API call includes them in the request body. The partnerUserSecret is sensitive and should be treated like a password. Copy both values immediately because the partnerUserSecret may be shown only once. Additionally, make note of your Expensify policy ID if you plan to create expenses — the policy ID is visible in the URL when you navigate to Settings → Policies → your policy name. You will need the policy ID to create expenses and reports under the correct company policy rather than under a personal account.

Pro tip: The partnerUserID is formatted like an email address (e.g., integrationUser@yourcompany.com) but it is not your regular Expensify login. The partnerUserSecret is a separate secret — store both in your .env file, never in code.

Expected result: You have your partnerUserID and partnerUserSecret copied from Expensify Settings → Integrations → API, ready to add to your .env file.

2

Add Credentials to .env and Create the Base API Route

Add your Expensify credentials to the .env file in your Bolt project. Unlike many APIs that use an Authorization Bearer header, Expensify's Integration Server requires credentials embedded in the request body JSON — never in headers. This is intentional: all Expensify API calls go through a single POST endpoint (https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations) with the credentials included in a credentials object inside the request body. Create a helper function that wraps the Expensify API call pattern: it accepts a command string and any additional parameters, constructs the full request body including credentials, POSTs to the Integration Server, and returns the parsed response. This helper becomes the foundation for all your Expensify API routes. The request body must be sent as application/x-www-form-urlencoded with a requestJobDescription parameter containing the JSON — this is unusual and differs from most modern APIs that accept application/json. The requestJobDescription parameter value is a JSON string (not an object) containing type, credentials, and command fields. Bolt generates this wrapper correctly when you describe the Expensify Integration Server's specific request format in your prompt.

Bolt.new Prompt

Set up Expensify Integration Server API access. Add EXPENSIFY_PARTNER_USER_ID and EXPENSIFY_PARTNER_USER_SECRET to .env with placeholder values. Create a utility file at lib/expensify.ts that exports an async function callExpensify(command: object) which POSTs to https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations as application/x-www-form-urlencoded with a requestJobDescription parameter containing JSON with type: 'get', credentials: { partnerUserID, partnerUserSecret }, and the command object spread in. Return the parsed JSON response. Handle HTTP errors by throwing with the response status.

Paste this in Bolt.new chat

lib/expensify.ts
1// lib/expensify.ts
2const EXPENSIFY_API_URL = 'https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations';
3
4interface ExpensifyCommand {
5 type: string;
6 [key: string]: unknown;
7}
8
9export async function callExpensify(command: ExpensifyCommand) {
10 const credentials = {
11 partnerUserID: process.env.EXPENSIFY_PARTNER_USER_ID!,
12 partnerUserSecret: process.env.EXPENSIFY_PARTNER_USER_SECRET!,
13 };
14
15 const requestJobDescription = JSON.stringify({
16 type: command.type,
17 credentials,
18 ...command,
19 });
20
21 const body = new URLSearchParams();
22 body.append('requestJobDescription', requestJobDescription);
23
24 const response = await fetch(EXPENSIFY_API_URL, {
25 method: 'POST',
26 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
27 body: body.toString(),
28 });
29
30 if (!response.ok) {
31 throw new Error(`Expensify API error: ${response.status} ${response.statusText}`);
32 }
33
34 return response.json();
35}

Pro tip: Expensify's Integration Server uses application/x-www-form-urlencoded, not application/json. This catches many developers off guard. The requestJobDescription field value is a JSON string embedded inside the form-encoded body.

Expected result: The lib/expensify.ts utility is in place and your .env file has placeholder credentials. The helper function handles the unusual Expensify request format automatically for all subsequent API routes.

3

Build the Reports Fetching API Route

Fetching expense reports from Expensify uses the GetReportIDList command to get report IDs and the GetReport command to retrieve full report details. The GetReportIDList command accepts filter parameters: startDate, endDate, statusFilters (a comma-separated list of report statuses such as 'APPROVED', 'REIMBURSED', 'OPEN', 'SUBMITTED'), and optionally an employeeEmail to filter by submitter. This command returns a list of report IDs matching the filter criteria. You then need to call GetReport for each ID to retrieve the full report details including the transaction list, total amount, submitter information, and current status. For a dashboard showing the last 30 days of reports, prompt Bolt to create a route that calls GetReportIDList with appropriate date filters and then fetches a reasonable number of report details. Avoid fetching more than 20-30 reports in a single request to prevent timeout issues. The route should return a structured array of report summaries that the dashboard component can render directly. Note that Expensify API responses sometimes include both a responseCode field (200 for success, 410 for error) and the actual data — always check responseCode before processing the response data to handle API-level errors that come back with HTTP 200 status codes.

Bolt.new Prompt

Create an API route at /api/expensify/reports that calls Expensify's Integration Server. First call the GetReportIDList command with startDate (30 days ago in YYYY-MM-DD format) and statusFilters: 'APPROVED,REIMBURSED,OPEN,SUBMITTED'. Then for each report ID (limit to 20 most recent), call the GetReport command. Return an array of report objects with: reportID, reportName, total (in dollars), status, submitter email, and submittedDate. Handle Expensify errors by checking responseCode in the response. Use the callExpensify helper from lib/expensify.ts.

Paste this in Bolt.new chat

app/api/expensify/reports/route.ts
1// app/api/expensify/reports/route.ts
2import { NextResponse } from 'next/server';
3import { callExpensify } from '@/lib/expensify';
4
5export async function GET() {
6 try {
7 const endDate = new Date().toISOString().split('T')[0];
8 const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
9
10 const listResponse = await callExpensify({
11 type: 'get',
12 command: 'GetReportIDList',
13 inputSettings: {
14 startDate,
15 endDate,
16 statusFilters: 'APPROVED,REIMBURSED,OPEN,SUBMITTED',
17 },
18 });
19
20 if (listResponse.responseCode !== 200) {
21 return NextResponse.json({ error: listResponse.responseMessage }, { status: 400 });
22 }
23
24 const reportIDs: string[] = (listResponse.reportIDs || []).slice(0, 20);
25
26 const reports = await Promise.all(
27 reportIDs.map(async (reportID) => {
28 const detail = await callExpensify({
29 type: 'get',
30 command: 'GetReport',
31 reportID,
32 });
33 return {
34 reportID,
35 reportName: detail.reportName || `Report ${reportID}`,
36 total: (detail.total || 0) / 100,
37 status: detail.status,
38 submitter: detail.submitter,
39 submittedDate: detail.submittedDate,
40 };
41 })
42 );
43
44 return NextResponse.json({ reports });
45 } catch (error) {
46 const message = error instanceof Error ? error.message : 'Unknown error';
47 return NextResponse.json({ error: message }, { status: 500 });
48 }
49}

Pro tip: Expensify stores monetary amounts as integers in cents (or the smallest currency unit). Divide by 100 to display dollar amounts with cents. Always check this in the API documentation for the specific command you are using.

Expected result: GET /api/expensify/reports returns an array of report objects with status, total, submitter, and date for the past 30 days. You can see these in Bolt's preview network tab.

4

Create Expense Transactions via API

Creating expenses programmatically uses the CreateTransaction command, which accepts the core fields of an expense: amount, currency, merchant, created (date in YYYY-MM-DD format), comment, category, and the policyID that the expense belongs to. The amount field should be passed in cents (integer) — multiply the dollar amount by 100. The employee email field (employeeEmail) associates the expense with a specific user in your Expensify policy. If no employeeEmail is specified, the expense is created under the credentials account holder. After creating a transaction, you may want to automatically create an expense report and add the transaction to it using the CreateReport command, then submit the report using the UpdateReport command with action: 'submit'. This three-step process — create transaction, create report, submit report — mirrors the workflow an employee follows manually in Expensify and is the standard programmatic submission flow. Bolt generates these three API calls when you describe the workflow clearly in your prompt. Because these are outbound HTTP calls to Expensify's server, they work during development in Bolt's WebContainer preview — you will see real data appear in your Expensify account after running them.

Bolt.new Prompt

Create an API route at /api/expensify/create-expense that accepts POST requests with: amount (dollars, number), merchant (string), date (YYYY-MM-DD), category (string), comment (string), and employeeEmail (string). Convert amount to cents and call Expensify's CreateTransaction command. Then create an expense report with CreateReport using the transaction ID and the policyID from process.env.EXPENSIFY_POLICY_ID. Finally, submit the report with UpdateReport action: 'submit'. Return the reportID on success. Add EXPENSIFY_POLICY_ID to .env.

Paste this in Bolt.new chat

app/api/expensify/create-expense/route.ts
1// app/api/expensify/create-expense/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { callExpensify } from '@/lib/expensify';
4
5export async function POST(request: NextRequest) {
6 const { amount, merchant, date, category, comment, employeeEmail } = await request.json();
7
8 try {
9 // Step 1: Create the transaction
10 const transaction = await callExpensify({
11 type: 'create',
12 command: 'CreateTransaction',
13 transactionList: [{
14 created: date,
15 currency: 'USD',
16 merchant,
17 amount: Math.round(amount * 100),
18 category,
19 comment,
20 ...(employeeEmail ? { employeeEmail } : {}),
21 }],
22 policyID: process.env.EXPENSIFY_POLICY_ID,
23 });
24
25 if (transaction.responseCode !== 200) {
26 return NextResponse.json({ error: transaction.responseMessage }, { status: 400 });
27 }
28
29 // Step 2: Create an expense report
30 const report = await callExpensify({
31 type: 'create',
32 command: 'CreateReport',
33 policyID: process.env.EXPENSIFY_POLICY_ID,
34 reportName: `${merchant} - ${date}`,
35 transactionIDList: transaction.transactionIDList,
36 });
37
38 return NextResponse.json({ reportID: report.reportID });
39 } catch (error) {
40 const message = error instanceof Error ? error.message : 'Unknown error';
41 return NextResponse.json({ error: message }, { status: 500 });
42 }
43}

Pro tip: Find your Expensify policyID by navigating to Settings → Policies → your policy name in Expensify. The policy ID appears in the URL as a long alphanumeric string. Add it to .env as EXPENSIFY_POLICY_ID.

Expected result: POST /api/expensify/create-expense creates a new expense transaction and report in your Expensify account. Verify by logging into Expensify and checking the Reports section.

5

Build the Expense Dashboard UI and Deploy

With the API routes in place, prompt Bolt to build the dashboard interface that connects to them. The dashboard should show a summary section at the top with three metric cards: total pending amount (reports with SUBMITTED or OPEN status), total approved and awaiting reimbursement, and total reimbursed this month. Below the metrics, show a table of recent expense reports with columns for report name, submitter, date, total amount, and status. Add a status badge with color coding (yellow for pending, green for approved, blue for reimbursed). Include an inline 'New Expense' button that opens a modal form with fields for merchant, amount, date, and category. Submitting the form calls your /api/expensify/create-expense route and refreshes the report list. The entire dashboard works in Bolt's WebContainer during development since it only makes outbound API calls to Expensify. For deployment, push to GitHub and connect to Netlify or Vercel. Set EXPENSIFY_PARTNER_USER_ID, EXPENSIFY_PARTNER_USER_SECRET, and EXPENSIFY_POLICY_ID as environment variables in your hosting dashboard. Expensify does not send webhook callbacks in the traditional sense for this API, so there are no incoming connections to configure — the WebContainer limitation for incoming webhooks does not apply here.

Bolt.new Prompt

Build an expense dashboard at /expenses using my Expensify API routes. Fetch reports from /api/expensify/reports on page load. Show three summary cards at the top: total OPEN+SUBMITTED amount (yellow), total APPROVED amount (green), and total REIMBURSED amount (blue). Below, render a table of reports with columns: Report Name, Submitter, Date, Amount (formatted as USD), Status (colored badge). Add a 'New Expense' button that opens a modal with a form (merchant, amount, date, category dropdown). Submitting the form POSTs to /api/expensify/create-expense and refreshes the table. Show loading states and empty states.

Paste this in Bolt.new chat

Pro tip: Expense dashboard data does not need to be real-time — add a 5-minute cache (revalidate: 300) to your reports API route so the dashboard does not hammer the Expensify API on every page load. Add a manual Refresh button for users who need current data.

Expected result: The /expenses dashboard displays your Expensify reports with summary totals, a sortable table, and a working New Expense modal. After deployment, it works identically on your production URL.

Common use cases

Expense Report Dashboard

Build a dashboard that shows all pending, approved, and reimbursed expense reports from your Expensify account. Filter by employee, date range, and status. Display total spend per month with a chart and show which reports are awaiting approval. This gives managers visibility into expense activity without logging into Expensify directly.

Bolt.new Prompt

Build an expense dashboard that calls my Expensify API route at /api/expensify/reports. The route should use the Expensify Integration Server API with my partnerUserID and partnerUserSecret from .env to call the GetReportIDList command and return all reports from the last 30 days with their status, total amount, and submitter name. Display the reports in a table sortable by date and amount. Add a summary bar showing total pending, approved, and reimbursed amounts. Use a Recharts bar chart to show spend by week.

Copy this prompt to try it in Bolt.new

Automated Expense Creation from External Data

Automatically create Expensify expenses when certain triggers happen in your application — for example, when a customer order is fulfilled and requires a shipping fee to be recorded, or when a project milestone generates a billable expense. The API route receives the trigger data and creates an expense in the correct Expensify policy.

Bolt.new Prompt

Create an API route at /api/create-expense that accepts POST requests with fields: amount (number), merchant (string), date (ISO string), category (string), and employeeEmail (string). The route should call Expensify's Integration Server API with the CreateTransaction command to create a new expense in the default policy. Use partnerUserID and partnerUserSecret from .env. Return the created expense ID on success. Call this route from a button on my order management page to log shipping costs as expenses.

Copy this prompt to try it in Bolt.new

Employee Expense Submission Portal

Build a simple expense submission form in Bolt that employees fill out to submit receipts and expense details. The form submits to a server-side route that creates the expense in Expensify and optionally creates a new expense report and submits it for approval — all without requiring employees to log into Expensify.

Bolt.new Prompt

Create an expense submission form with fields for merchant name, amount, date, expense category (dropdown: Travel, Meals, Software, Office Supplies), a description field, and a receipt image upload (store in Supabase Storage and pass the URL to Expensify). When submitted, POST to /api/submit-expense which creates the expense via Expensify API using CreateTransaction and then creates and submits a new expense report using CreateReport. Show a success message with the report ID. Use my Expensify partnerUserID and partnerUserSecret from .env.

Copy this prompt to try it in Bolt.new

Troubleshooting

Expensify API returns responseCode 410 with message 'Incorrect login or password'

Cause: The partnerUserID or partnerUserSecret in your .env file does not match your Integration Server credentials, or the credentials have been rotated.

Solution: Log in to Expensify and navigate to Settings → Integrations → API. Verify your partnerUserID matches exactly (it is case-sensitive and formatted like an email). If you suspect the secret was changed, regenerate it on the API settings page and update your .env file. Restart the Bolt development server after changing .env values.

API calls return HTML instead of JSON, or a 'requestJobDescription is required' error

Cause: The request body is being sent as application/json instead of application/x-www-form-urlencoded, or the requestJobDescription field is missing from the form body.

Solution: Expensify's Integration Server requires application/x-www-form-urlencoded content type with the requestJobDescription field containing a JSON string. Use URLSearchParams to build the body and set Content-Type to application/x-www-form-urlencoded explicitly. Do not use JSON.stringify on the entire body — only on the value of requestJobDescription.

typescript
1const body = new URLSearchParams();
2body.append('requestJobDescription', JSON.stringify({ type, credentials, command }));
3await fetch(EXPENSIFY_API_URL, {
4 method: 'POST',
5 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
6 body: body.toString(),
7});

GetReportIDList returns an empty array even though reports exist in Expensify

Cause: The date range filter is incorrect (future dates, wrong format, or the reports fall outside the 30-day window), or the statusFilters parameter does not include the statuses of existing reports.

Solution: Verify that startDate and endDate are in YYYY-MM-DD format. Broaden the date range to 90 days for initial testing. Include all status values in statusFilters: 'APPROVED,REIMBURSED,OPEN,SUBMITTED,ARCHIVED'. Check in Expensify that there are reports in the date range under the account associated with your partnerUserID.

CreateTransaction succeeds but the new expense does not appear in Expensify's web interface

Cause: The transaction was created under the credentials account rather than being visible in the expected policy, or the policyID is incorrect.

Solution: Check the Expensify account associated with your partnerUserID directly (log in as that user). The transaction may be in the inbox rather than a named report. Verify that your EXPENSIFY_POLICY_ID matches the policy visible at Settings → Policies. If using a team policy, ensure the partnerUserID account is a member of that policy.

Best practices

  • Cache Expensify API responses for at least 5 minutes — the Integration Server has rate limits and expense data rarely changes second-to-second
  • Store expense IDs and report IDs returned from Expensify in your own Supabase database so you can reference them later for updates, deletions, and audit trails
  • Always validate amounts server-side before sending to Expensify — a negative amount or non-numeric value causes confusing errors
  • Use your Expensify sandbox account for development testing if available — creating real expenses in production during development clutters your expense reports
  • Implement idempotency by checking for a duplicate expense before creating a new one — compare merchant, amount, and date to avoid double submissions
  • Handle the Expensify responseCode field explicitly in every API call — a successful HTTP 200 response can still contain an application-level error in the responseCode field
  • Log all Expensify API calls with timestamps and response codes in your server logs for debugging — the Integration Server errors can be cryptic without context

Alternatives

Frequently asked questions

Does Expensify's Integration Server API require a paid subscription?

Yes. Integration Server API access is available on Expensify's Professional ($9/user/month) and Control ($18/user/month) plans. The free Expensify tier does not include programmatic API access. Before building an integration, confirm your account tier includes API access by checking whether Settings → Integrations → API is visible in your account.

Why does Expensify's API use such an unusual request format?

Expensify's Integration Server was designed before JSON REST APIs became the standard. It uses a command-based architecture with application/x-www-form-urlencoded requests because this pattern was common in older enterprise systems. The format is consistent — every request uses the same endpoint and credential structure — but it does require wrapping the JSON in a URL-encoded form field, which surprises developers used to modern REST APIs.

Can I test Expensify API calls in Bolt's WebContainer preview?

Yes. Expensify's Integration Server API calls are outbound HTTP requests from your API route to Expensify's servers. The WebContainer supports outbound requests, so you can create expenses, fetch reports, and test the full API workflow during development without deploying. The WebContainer limitation only applies to incoming webhooks — Expensify's Integration Server API does not send incoming webhooks to your app.

How do I handle Expensify API rate limiting in my Bolt app?

Expensify does not publish specific rate limits, but the Integration Server can return errors for excessive requests. Add response caching to your API routes (at minimum 5 minutes for read-heavy routes like GetReportIDList). For write operations, implement a debounce on the submit button to prevent duplicate submissions. If you receive unexpected errors, add a retry with exponential backoff for non-critical read requests.

Can I import receipt images into Expensify through the API?

Yes, but indirectly. You can include a receipt_url field in the CreateTransaction command pointing to a publicly accessible image URL. Store the receipt image in Supabase Storage or another cloud storage with a public URL, then pass that URL when creating the Expensify transaction. Expensify will fetch and attach the image to the expense as a receipt.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.