How to Categorize Emailed Expense Receipts with AI and Log to MongoDB
Forward a receipt to a dedicated Spojit address and have an AI agent read the attached PDF or image, sort it into a structured expense category, and store it as a document in MongoDB.
What This Integration Does
Finance and operations teams drown in one-off receipts: a taxi PDF here, a software invoice image there, a coffee-shop scan forwarded from someone's phone. Manually typing each one into a spreadsheet or accounting system is slow and error prone. This workflow gives every receipt a single inbox. The moment one arrives, Spojit fetches the attachment, lets a Knowledge node in Query mode read it, and writes a clean, categorized record into a MongoDB collection that you can report on, reconcile, or push downstream later.
The workflow is triggered by a Mailhook: Spojit generates a unique address, and any mail sent to it starts a run within seconds. The receipt file is pulled with an Attachment node, embedded into a per-run Transient Knowledge collection, and read back with an AI query that returns structured fields (vendor, date, total, currency, category). A Connector node then inserts that record into MongoDB using the insert-documents tool. Runs are independent and async: each forwarded email produces exactly one document, deduplicated per message, and the transient collection is discarded automatically when the run finishes, so nothing lingers between receipts.
Prerequisites
- A MongoDB connection added under Connections, pointing at the database where you want receipts logged (for example a database named
finance). - A target collection name decided in advance, for example
expense_receipts. MongoDB creates the collection on first insert, so it does not need to exist yet. - A short list of the expense categories your business uses (for example
Travel,Meals,Software,Office Supplies,Other) so the AI maps to a known set rather than inventing labels. - Receipts that arrive as PDF or image attachments. The Knowledge node can read PDFs and images via OCR.
Step 1: Create the Mailhook trigger
Add a Trigger node and set Trigger Type to Mailhook. Optionally set an Address prefix such as receipts (1-24 characters), then choose Generate email address. Spojit produces a unique address like receipts-1a2b3c4d5e6f7g8h@mailhook.spojit.com. Copy it and use it as the destination for forwarded receipts: paste it into staff bookmarks, or add a forwarding rule in your shared inbox so anything tagged "receipt" lands here. No mailbox connection is required. The trigger fires whether the address is in To, Cc, or Bcc, and each message is deduplicated so a forwarded thread does not double-log.
The trigger output is available as {{ input }} and includes {{ input.from }}, {{ input.subject }}, {{ input.receivedAt }}, {{ input.replyTo }}, and {{ input.attachments }} (each attachment a reference of { id, filename, contentType }).
Step 2: Fetch the receipt file with an Attachment node
Add an Attachment node. It only saves in a workflow that has a Mailhook trigger, so place it right after the trigger. Configure it to pull the receipt itself:
- Mode:
Single(grab the first matching file as a single object). - Content type:
application/pdf, image/*so both PDF receipts and photo scans qualify. - Filename pattern: leave broad, or set something like
*.pdfif you only accept PDFs. - Fail if no attachment matches: turn this on so a receipt-less email stops cleanly instead of inserting an empty record.
In Single mode the node outputs { filename, contentType, size, content }, where content is the base64 body of the file. Keep within the per-attachment and per-run size limits (10 MB and 25 MB by default). Name the output variable, for example receipt, so later steps can reference {{ receipt.content }}.
Step 3: Embed the receipt into a Transient collection
Add a Knowledge node in Embed mode. This turns the raw file into searchable content for the AI query in the next step.
- Collection:
Transient. A transient collection is created just for this run, shared across nodes in the same run, and cleaned up automatically when the run ends, which is exactly right for a single throwaway receipt. - Document Type:
PDFfor PDF receipts, orImages via OCRfor photographed receipts. If your senders mix both, you can branch with a Condition node on{{ receipt.contentType }}and embed each type appropriately. - Document Input:
{{ receipt.content }}(the base64 body straight from the Attachment node).
No file name is required for a transient collection. Set an Output Variable if you want the chunk count for logging, but the embedded content is the important result here.
Step 4: Extract structured fields with a Knowledge query
Add a second Knowledge node in Query mode, also pointed at the Transient collection so it reads the document embedded in Step 3. Set a Prompt that tells the Knowledge node what to pull out and which categories to use:
Read this expense receipt and return the vendor name, the
transaction date in YYYY-MM-DD format, the total amount as a
number, the 3-letter currency code, and a category chosen from
exactly this list: Travel, Meals, Software, Office Supplies, Other.
If a value is missing, return null for it.
To force a clean, predictable record, fill in the Response Schema so the output is structured JSON rather than prose:
{
"type": "object",
"properties": {
"vendor": { "type": "string" },
"date": { "type": "string" },
"total": { "type": "number" },
"currency": { "type": "string" },
"category": { "type": "string" }
},
"required": ["vendor", "total", "category"]
}
Set a Model for synthesis and a small Result Count (the default of 5 is plenty for a single receipt). Name the Output Variable, for example extracted, so the fields are available as {{ extracted.vendor }}, {{ extracted.total }}, and so on.
Step 5: Insert the record into MongoDB
Add a Connector node in Direct mode, choose your MongoDB connection, and select the insert-documents tool. Set collection to expense_receipts and map documents to a single-element array built from the extracted fields plus useful context from the trigger:
[
{
"vendor": "{{ extracted.vendor }}",
"date": "{{ extracted.date }}",
"total": "{{ extracted.total }}",
"currency": "{{ extracted.currency }}",
"category": "{{ extracted.category }}",
"filename": "{{ receipt.filename }}",
"submittedBy": "{{ input.from }}",
"subject": "{{ input.subject }}",
"receivedAt": "{{ input.receivedAt }}"
}
]
The tool returns insertedCount, which should be 1 per receipt. Including filename and submittedBy gives you an audit trail back to the original email if a figure ever needs checking.
Step 6: Confirm receipt back to the sender (optional)
Add a Send Email node so the person who forwarded the receipt gets a confirmation. Because a Mailhook never replies to the sender on its own, this node is how you close the loop. Set Recipients to {{ input.replyTo }}, a templated Subject like Receipt logged: {{ extracted.vendor }}, and a short body summarizing what was recorded:
Thanks. Your receipt from {{ extracted.vendor }} for
{{ extracted.total }} {{ extracted.currency }} was categorized
as {{ extracted.category }} and logged.
Remember that external recipients must be on your organization allowlist under Settings > General > Email recipients, and Send Email counts toward your monthly email allowance. To send from your own domain instead, use the Resend or SMTP connector.
Tips
- Add the same category list to both the prompt and the Response Schema description so the agent stays inside your chart of accounts instead of inventing labels.
- If you accept multiple receipts per email, set the Attachment node to
Multiplemode, then use a Loop over{{ receipt.attachments }}and run the embed, query, and insert steps once per file. - Use a From allowlist on the Mailhook trigger so only your team's addresses can create records, which keeps stray or spam mail out of your finance collection.
- Let Miraxa scaffold the canvas first. A prompt such as "Build a workflow that watches a mailhook, pulls the PDF attachment, extracts vendor, date, total, and category, and inserts a document into MongoDB" gets you most of the structure, then fine-tune fields in the properties panel.
Common Pitfalls
- Empty inserts from receipt-less emails. If you leave Fail if no attachment matches off, a thank-you reply with no file still runs the workflow. Turn it on so the run stops at the Attachment node.
- Wrong document type for photos. A phone snapshot embedded as
PDFwill not read. Match the Knowledge Document Type to the file:Images via OCRfor JPEG and PNG receipts. - Numbers stored as text. Without a Response Schema the model may return
"$42.50". Keeptotaltyped asnumberin the schema so MongoDB stores a value you can sum and report on. - Mailhook address rotation. Choosing Regenerate address kills the old address instantly. If you regenerate, update every forwarding rule and bookmark, or receipts sent to the old address are silently dropped.
Testing
Forward a single known receipt to the generated Mailhook address and watch the run in your execution history. Check the Attachment node captured the file ({{ receipt.filename }} and a non-zero size), the Knowledge query produced the expected fields under {{ extracted }}, and the MongoDB step returned insertedCount of 1. Then run find-documents on expense_receipts with a filter like { "vendor": "..." } to confirm the document landed with correctly typed values. Once one receipt round-trips cleanly, send a small batch covering both PDF and image formats before sharing the address with your wider team.
Learn More
- Mailhook trigger documentation
- Attachment node documentation
- Knowledge collections and transient queries
- MongoDB connector documentation
- How to Extract and Store Invoice Data in a Knowledge Collection
- How to Use AI to Categorize and Code Expenses Automatically
- How to Create Shopify Orders from PO PDFs Emailed to a Mailhook