Manual expense reimbursement is a tax on your team’s time. Someone photographs a receipt, emails it, someone else opens it, types numbers into a spreadsheet, then pings a manager. Every step is a handoff that can break.
This tutorial walks through an n8n workflow that eliminates most of those handoffs. It parses receipt images via API, routes expenses by amount, logs approvals to Google Sheets, and notifies submitters via Slack or Gmail. No code required.
️ What You’ll Build
The workflow does five things in sequence:
- Triggers when a receipt image is submitted via webhook, Gmail, or Google Drive
- Calls the Receipt Parser API and gets back structured JSON
- Routes based on amount: auto-approves expenses under $50, flags anything over for manager review
- Logs every expense to a Google Sheet
- Notifies the submitter via Slack or email with the parsed details
Prerequisites
- An n8n instance, cloud or self-hosted (n8n.io)
- A Receipt Parser API key from ilovesreceipt.com (free tier: 500 calls/month, no credit card required)
- A Google account for Sheets logging
- Optional: a Slack workspace for notifications

Step 1: Set Up the Trigger
Pick your entry point based on how receipts arrive. Three options:
- Webhook (recommended): Add a
Webhooknode, set method to POST. Any tool that can send a webhook, including forms, mobile apps, and Zapier, can trigger this workflow. - Gmail: Add a
Gmail Triggernode. Filter by subject containing “receipt” or “reimbursement”. Fires on each matching email with an attachment. - Google Drive: Add a
Google Drive Triggernode. Watch a specific folder such as/Receipts/Pending. Fires when any new file is uploaded.
The rest of this tutorial uses the webhook option since it’s the most reusable across intake methods.
Step 2: Read the File
No conversion step needed. The Receipt Parser API accepts the raw file as multipart/form-data, so base64 encoding is not required.
If your trigger provides a URL (for example, a Google Drive file URL), add an HTTP Request node set to GET to download the binary first. If your trigger delivers a binary attachment directly, such as a Gmail attachment, pipe it straight to Step 3.
Step 3: Call the Receipt Parser API
Add an HTTP Request node with these settings:
| Field | Value |
|---|---|
| Method | POST |
| URL | https://web-production-58295.up.railway.app/api/parse |
| Authentication | Header Auth |
| Header name | Authorization |
| Header value | Bearer {{ $credentials.receiptParserKey }} |
| Body Content Type | Form Data (multipart) |
| Body field name | file |
| Body field value | Binary data from previous node |
Store your API key in n8n Credentials as a Generic Credential with Authorization → Bearer YOUR_KEY. This keeps it out of the workflow JSON and reusable across other flows. Get a free key at ilovesreceipt.com.
After this node runs, structured data is available in subsequent nodes as $json.data.merchant.name, $json.data.total, and so on.
Step 4: Route by Amount (IF Node)
Add an IF node with this condition:
{{ $json.data.total }} > 50- True branch: flag for manager review
- False branch: auto-approve and log
You can layer additional conditions if needed: category-based routing (meals vs. travel vs. supplies), a merchant allowlist or blocklist, or employee-specific thresholds.

Step 5: Log to Google Sheets
On the False (auto-approved) branch, add a Google Sheets node. Set Operation to Append Row, point it at your expense log spreadsheet, and map these columns:
| Column | Value |
|---|---|
| Date | {{ $json.data.date }} |
| Merchant | {{ $json.data.merchant.name }} |
| Total | {{ $json.data.total }} |
| Tax | {{ $json.data.tax }} |
| Tip | {{ $json.data.tip }} |
| Payment | {{ $json.data.payment_method }} |
| Status | Auto-Approved |
| Submitted | {{ $now }} |
Step 6: Flag Large Expenses in Slack
On the True (high expense) branch, add a Slack node. Set Operation to Send Message, target the #expense-approvals channel, and use this message template:
*Expense Approval Required*
*Merchant:* {{ $json.data.merchant.name }}
*Amount:* ${{ $json.data.total }}
*Date:* {{ $json.data.date }}
*Payment:* {{ $json.data.payment_method }}
React ✅ to approve or ❌ to reject.Step 7: Notify the Submitter
On both branches, add a Gmail or Slack node to confirm receipt. Use this message body:
Hi there — your expense was received and parsed successfully.
Merchant: {{ $json.data.merchant.name }}
Date: {{ $json.data.date }}
Total: ${{ $json.data.total }}
{{ $json.data.total > 50 ? "Your expense has been flagged for manager review." : "Your expense has been auto-approved and logged." }}Testing the Workflow
Open the workflow in n8n and click Execute Workflow with test mode on. Then send a POST request to your webhook URL with a receipt image:
curl -X POST https://your-n8n-instance.com/webhook/receipt-parse
-F "[email protected]"Check your Google Sheet for the logged row and your Slack channel for any approval notifications.
Going Further
Once the core workflow is running, a few extensions are worth considering:
- Multi-currency support: The API detects currency. Add a conversion step using an exchange rate API to normalize totals.
- PDF invoices: The API handles PDFs too, useful for contractor invoices submitted via email attachment.
- Airtable instead of Sheets: Swap the Google Sheets node for an Airtable node for richer filtering and views.
- Approval loop: Use n8n’s
Waitnode to pause the workflow until a Slack reaction is received before marking the expense as approved.
Common Pitfalls
- Binary data not passing through: Make sure the node connecting to the HTTP Request node is set to pass binary data, not just JSON. Check the node’s output panel to confirm a binary item is present.
- API key exposed in workflow JSON: Always use n8n Credentials for the API key. Pasting it directly into the node makes it visible to anyone who exports the workflow.
- IF node evaluating strings instead of numbers: If
$json.data.totalcomes back as a string, wrap it inNumber()or use n8n’s expression editor to cast it before the comparison.
