Set Up a Gmail Receipt Monitor on a Mac
A step-by-step guide for running a local Mac service that checks Gmail for receipt emails, downloads receipt PDFs or creates PDFs from emails, and saves them in your Downloads folder with useful filenames.
~/GmailReceiptMonitor. Do not run it from ~/Documents/GmailReceiptMonitor; macOS privacy restrictions can interfere with background LaunchAgents running from Documents.What this does
The monitor checks Gmail every five minutes for messages matching this Gmail search:
label:ReceiptsToDownload newer_than:30d
For each matching message, it saves a PDF to your Downloads folder using this format:
XX.XX YYYY-MM-DD Vendor Description receipt.pdf
Examples:
8.00 2026-05-25 Wi-Fi Onboard Here's Wi-Fi Onboard - Check receipt.pdf
18.07 2026-05-07 Zoom Communications Zoom Workplace Pro receipt.pdf
5.00 2026-04-30 Nataki Garrett Myers Be a Ladder Leader receipt.pdf
1. Requirements
- A Mac.
- Terminal access.
- Python 3 installed.
- A Gmail account.
- The monitor script files:
gmail_receipt_monitor.pyandvendor_map.json.
Open Terminal from Applications → Utilities → Terminal.
2. Create the Gmail label
The script only processes messages with a specific Gmail label. This gives you control over what gets downloaded.
- Open Gmail in a browser.
- Click the gear icon.
- Click See all settings.
- Open the Labels tab.
- Create a new label named exactly:
ReceiptsToDownload
You can apply that label manually to receipt emails, or create Gmail filters for common vendors.
3. Create Google API credentials
The monitor uses the Gmail API. You need a local OAuth credential file called credentials.json.
- Go to Google Cloud Console.
- Create a new project. A name like Receipt Finder is fine.
- Enable the Gmail API for that project.
- Configure the OAuth consent screen.
- Keep the app in testing mode for personal use.
- Add your Gmail address as a test user.
- Create an OAuth Client ID.
- Choose Desktop app, not Web Application.
- Download the credential file.
- Rename it to
credentials.json.
4. Install the monitor folder
Create the final working folder:
mkdir -p ~/GmailReceiptMonitor
Copy the needed files into that folder. Adjust these commands if your files are somewhere else:
cp ~/Downloads/gmail_receipt_monitor.py ~/GmailReceiptMonitor/
cp ~/Downloads/vendor_map.json ~/GmailReceiptMonitor/
cp ~/Downloads/credentials.json ~/GmailReceiptMonitor/
Make the script executable:
chmod +x ~/GmailReceiptMonitor/gmail_receipt_monitor.py
5. Create the Python environment
Create an isolated Python environment and install the required packages:
cd ~/GmailReceiptMonitor
python3 -m venv venv
~/GmailReceiptMonitor/venv/bin/pip install --upgrade pip
~/GmailReceiptMonitor/venv/bin/pip install \
google-api-python-client \
google-auth-httplib2 \
google-auth-oauthlib \
beautifulsoup4 \
weasyprint \
pypdf
pypdf is needed because vendors such as Zoom may put the correct total inside an attached PDF invoice.
6. Authorize Gmail access
Run the script once manually. This opens a browser window and asks you to grant Gmail read-only access.
~/GmailReceiptMonitor/venv/bin/python \
~/GmailReceiptMonitor/gmail_receipt_monitor.py \
--dry-run \
--max-messages 1
After authorization, the script creates:
~/GmailReceiptMonitor/token.json
token.json. It contains live OAuth credentials. Treat it like a password.7. Test manually
First do a dry run. This shows what would be saved without actually saving files.
~/GmailReceiptMonitor/venv/bin/python \
~/GmailReceiptMonitor/gmail_receipt_monitor.py \
--dry-run \
--max-messages 25 \
--reprocess
Then do a real run:
~/GmailReceiptMonitor/venv/bin/python \
~/GmailReceiptMonitor/gmail_receipt_monitor.py \
--max-messages 25
A successful run may look like this:
Found 19 candidate message(s).
Saved 0 receipt PDF(s).
Saved 0 is not automatically bad. It often means those messages were already processed and recorded in processed_receipts.json.
8. Create the wrapper script
The wrapper gives launchd a simple command to run and creates clean logs.
cat > ~/GmailReceiptMonitor/run_receipt_monitor.sh <<'EOF'
#!/bin/zsh
export HOME=/Users/YOUR_USERNAME
export PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
cd /Users/YOUR_USERNAME/GmailReceiptMonitor || exit 78
echo "----- $(date) starting gmail receipt monitor -----" >> /Users/YOUR_USERNAME/GmailReceiptMonitor/gmail_receipt_monitor.wrapper.log
/Users/YOUR_USERNAME/GmailReceiptMonitor/venv/bin/python \
/Users/YOUR_USERNAME/GmailReceiptMonitor/gmail_receipt_monitor.py \
--work-dir /Users/YOUR_USERNAME/GmailReceiptMonitor \
--download-dir /Users/YOUR_USERNAME/Downloads \
--query "label:ReceiptsToDownload newer_than:30d" \
--max-messages 25 \
>> /Users/YOUR_USERNAME/GmailReceiptMonitor/gmail_receipt_monitor.out.log \
2>> /Users/YOUR_USERNAME/GmailReceiptMonitor/gmail_receipt_monitor.err.log
STATUS=$?
echo "----- $(date) finished with status $STATUS -----" >> /Users/YOUR_USERNAME/GmailReceiptMonitor/gmail_receipt_monitor.wrapper.log
exit $STATUS
EOF
chmod +x ~/GmailReceiptMonitor/run_receipt_monitor.sh
Test the wrapper:
~/GmailReceiptMonitor/run_receipt_monitor.sh
echo $?
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.wrapper.log
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.out.log
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.err.log
You want echo $? to return 0.
9. Install the LaunchAgent
This makes macOS run the receipt monitor every five minutes.
cat > ~/Library/LaunchAgents/com.example.gmail-receipt-monitor.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.gmail-receipt-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/YOUR_USERNAME/GmailReceiptMonitor/run_receipt_monitor.sh</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/YOUR_USERNAME/GmailReceiptMonitor</string>
<key>StartInterval</key>
<integer>300</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/GmailReceiptMonitor/launchd.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/GmailReceiptMonitor/launchd.err.log</string>
</dict>
</plist>
EOF
Load and start the LaunchAgent:
UIDNUM=$(id -u)
launchctl bootout gui/$UIDNUM ~/Library/LaunchAgents/com.example.gmail-receipt-monitor.plist 2>/dev/null
plutil -lint ~/Library/LaunchAgents/com.example.gmail-receipt-monitor.plist
: > ~/GmailReceiptMonitor/launchd.out.log
: > ~/GmailReceiptMonitor/launchd.err.log
: > ~/GmailReceiptMonitor/gmail_receipt_monitor.out.log
: > ~/GmailReceiptMonitor/gmail_receipt_monitor.err.log
: > ~/GmailReceiptMonitor/gmail_receipt_monitor.wrapper.log
launchctl bootstrap gui/$UIDNUM ~/Library/LaunchAgents/com.example.gmail-receipt-monitor.plist
launchctl kickstart -k gui/$UIDNUM/com.example.gmail-receipt-monitor
10. Check that it is working
Use this quick health check:
launchctl list | grep gmail
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.wrapper.log
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.out.log
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.err.log
ls -lt ~/Downloads | head -20
A healthy idle service looks like this:
- 0 com.example.gmail-receipt-monitor
| Output | Meaning |
|---|---|
- 0 com.example.gmail-receipt-monitor | Loaded, idle, and last run succeeded. |
12345 0 com.example.gmail-receipt-monitor | Currently running, and last known status is good. |
- 78 com.example.gmail-receipt-monitor | The last run failed. Check the error logs. |
Force a run now:
launchctl kickstart -k gui/$(id -u)/com.example.gmail-receipt-monitor
sleep 15
launchctl list | grep gmail
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.wrapper.log
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.out.log
tail -40 ~/GmailReceiptMonitor/gmail_receipt_monitor.err.log
11. Troubleshooting
launchctl shows - 78
Check the logs:
tail -100 ~/GmailReceiptMonitor/gmail_receipt_monitor.err.log
tail -100 ~/GmailReceiptMonitor/launchd.err.log
launchctl print gui/$(id -u)/com.example.gmail-receipt-monitor | tail -120
If the error mentions Google hosts, test connectivity:
curl -I https://oauth2.googleapis.com
curl -I https://gmail.googleapis.com
dig oauth2.googleapis.com
dig gmail.googleapis.com
A 404 from curl -I is acceptable here. It proves the Mac can reach the Google server.
Manual run works, LaunchAgent fails from Documents
Move the whole monitor to:
~/GmailReceiptMonitor
Running from ~/Documents can trigger macOS privacy restrictions for background agents.
Saved 0 receipt PDF(s)
This is normal if the messages are already processed. The ledger is:
~/GmailReceiptMonitor/processed_receipts.json
To force a one-time reprocess:
~/GmailReceiptMonitor/venv/bin/python \
~/GmailReceiptMonitor/gmail_receipt_monitor.py \
--max-messages 25 \
--reprocess
WeasyPrint hangs while rendering an email
Some emails contain remote tracking images. If PDF generation hangs, interrupt the manual run with Control-C. A future hardening improvement is to block remote images during email-to-PDF rendering.
Substack receipts parse as 0.00
The parser should support whole-dollar formats such as:
$5 USD
Make sure you are using the patched script that includes the Substack whole-dollar fix.
Zoom receipts parse as 0.00
Zoom invoices can show both:
Invoice Total $18.07
Invoice Balance $0.00
The parser should prefer Invoice Total and ignore Invoice Balance. Make sure pypdf is installed:
~/GmailReceiptMonitor/venv/bin/pip install pypdf
Duplicate filenames
The filename should include a normalized date:
XX.XX YYYY-MM-DD Vendor Description receipt.pdf
If it does not, install the dated-filename version of the script.
12. Routine maintenance
Confirm the service is healthy:
launchctl list | grep gmail
Stop the service:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.example.gmail-receipt-monitor.plist
Start it again:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.gmail-receipt-monitor.plist
launchctl kickstart -k gui/$(id -u)/com.example.gmail-receipt-monitor
See recent downloads:
ls -lt ~/Downloads | head -20
launchctl list | grep gmail shows - 0 com.example.gmail-receipt-monitor, the monitor is installed, idle, and healthy.