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.

Final working setup: keep the monitor in ~/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.py and vendor_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.

  1. Open Gmail in a browser.
  2. Click the gear icon.
  3. Click See all settings.
  4. Open the Labels tab.
  5. 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.

  1. Go to Google Cloud Console.
  2. Create a new project. A name like Receipt Finder is fine.
  3. Enable the Gmail API for that project.
  4. Configure the OAuth consent screen.
  5. Keep the app in testing mode for personal use.
  6. Add your Gmail address as a test user.
  7. Create an OAuth Client ID.
  8. Choose Desktop app, not Web Application.
  9. Download the credential file.
  10. Rename it to credentials.json.
Important: if the OAuth app is in Testing mode, your own Gmail address must be added as a test user. Otherwise Google will block sign-in.

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
Do not share 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
OutputMeaning
- 0 com.example.gmail-receipt-monitorLoaded, idle, and last run succeeded.
12345 0 com.example.gmail-receipt-monitorCurrently running, and last known status is good.
- 78 com.example.gmail-receipt-monitorThe 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
Final confirmation: when launchctl list | grep gmail shows - 0 com.example.gmail-receipt-monitor, the monitor is installed, idle, and healthy.