How to Turn Scanned Receipt Photos into Structured Expense Records with AI OCR
Let employees email a photo of a receipt to a dedicated address, then have Spojit read the image, extract the vendor, amount, and date as clean JSON, store the record in MongoDB, and confirm in Slack.
What This Integration Does
Expense capture usually means someone typing numbers off a crumpled receipt into a spreadsheet days after the fact. This workflow removes that step entirely. An employee snaps a photo of a receipt, emails it to a Spojit Mailhook address, and within seconds the image is read by the intelligent layer across your automation, turned into a structured expense record, and saved to your database. No app install, no form, no manual data entry: if a person can send an email with a photo attached, they can file an expense.
The run is push-based and fully asynchronous. Any mail that arrives at the Mailhook address starts one execution. The Attachment node pulls the image bytes from that email, a transient Knowledge collection embeds and OCRs the image and queries it for the fields you want, and a Connector node inserts a single document into MongoDB. A Slack message confirms the capture. Because the Mailhook trigger never replies to the sender, the optional confirmation is a Send Email step back to {{ input.replyTo }} or, as shown here, a Slack note to your finance channel. Each email is deduplicated per message, so a forwarded or retried email will not create a duplicate run. Re-running the same workflow on a new receipt simply inserts another document; the transient collection is created fresh per run and discarded when the run finishes, so receipts never bleed into each other.
Prerequisites
- A MongoDB connection in Connections, pointing at the database that will hold your expenses, with rights to insert documents into an
expensescollection. - A Slack connection in Connections, authorized to post to the channel where you want capture confirmations.
- A workspace that can use Knowledge collections, and access to the Knowledge section of the sidebar.
- An internal email address or distribution list you control, so you can forward receipts to the Mailhook address (for example a finance inbox or a forwarding rule).
- A few sample receipt photos (JPG or PNG) to test with.
Step 1: Start with a Mailhook trigger
Create a new workflow and add a Trigger node. Set Trigger Type to Mailhook. Optionally set an Address prefix such as receipts (1 to 24 characters), then click Generate email address. Spojit produces a unique address like receipts-1a2b3c4d5e6f7g8h@mailhook.spojit.com. Copy it and share it with your team, or point a forwarding rule at it.
To keep noise out, expand the optional filters and add a From allowlist of staff domains, plus a Subject regex if you want to require a keyword. The trigger fires whether the address is in To, Cc, or Bcc. Its output is available downstream as {{ input }}, including {{ input.from }}, {{ input.subject }}, {{ input.replyTo }}, and the {{ input.attachments }} references.
Step 2: Fetch the receipt image with an Attachment node
Add an Attachment node directly after the trigger. The designer only allows this node when a Mailhook trigger is present. Set Mode to Single so you get the first matching image as a single object. Set the Content type filter to image/* so only photos are pulled, and optionally set a Filename pattern like *.jpg or *.png. Turn on Fail if no attachment matches so an email with no photo stops cleanly rather than inserting an empty record.
The node outputs { filename, contentType, size, content }, where content is the base64-encoded image. You will feed {{ attachment.content }} straight into the Knowledge node in the next step. Note the limits: 10 MB per attachment and 25 MB per run by default, which is ample for a phone photo.
Step 3: OCR and embed the image into a transient Knowledge collection
Add a Knowledge node in Embed mode. In the Collection dropdown, choose Transient so the collection is created just for this run and discarded when the run completes; no file name or fixed embedding model is required. Set Document Type to Images via OCR so the photo is read as text. For Document Input, pass the bytes from the previous step:
{{ attachment.content }}
Give the step an Output Variable such as embedded. When this node finishes, the receipt text lives in the run-scoped collection, ready to query in the next step. A transient collection is the right tool here because you embed one document, query it once, and never need it again.
Step 4: Extract vendor, amount, and date as structured JSON
Add a second Knowledge node, this time in Query mode. Set Collection to Transient so it reads the document embedded earlier in the same run. In the Prompt, ask for exactly the fields you need, for example: Extract the merchant or vendor name, the total amount paid, the currency, and the purchase date from this receipt. Choose a Model for synthesis and leave Result Count at its default.
The key to clean data is the Response Schema. Provide a JSON schema so the output is forced into a predictable shape rather than free text:
{
"type": "object",
"properties": {
"vendor": { "type": "string" },
"amount": { "type": "number" },
"currency": { "type": "string" },
"date": { "type": "string", "description": "ISO date, e.g. 2026-06-20" }
},
"required": ["vendor", "amount", "date"]
}
Set the Output Variable to receipt. You can now reference {{ receipt.vendor }}, {{ receipt.amount }}, {{ receipt.currency }}, and {{ receipt.date }} in later steps.
Step 5: Insert the expense record into MongoDB
Add a Connector node in Direct mode. Pick the MongoDB connector and the insert-documents tool. Set collection to expenses and map a single document into the documents array, combining the extracted fields with metadata from the trigger:
{
"collection": "expenses",
"documents": [
{
"vendor": "{{ receipt.vendor }}",
"amount": "{{ receipt.amount }}",
"currency": "{{ receipt.currency }}",
"purchaseDate":"{{ receipt.date }}",
"submittedBy": "{{ input.from }}",
"subject": "{{ input.subject }}",
"fileName": "{{ attachment.filename }}",
"receivedAt": "{{ input.receivedAt }}",
"status": "captured"
}
]
}
Direct mode is correct here because this is a single, predictable insert with no AI judgment, so it costs no AI credits. Give the step an Output Variable such as inserted so you can reference the result downstream.
Step 6: Confirm the capture in Slack
Add a final Connector node in Direct mode, choose the Slack connector, and select the send-message tool. Set the channel to your finance channel and build the text from the extracted fields so reviewers can sanity-check at a glance:
New expense captured: {{ receipt.vendor }} - {{ receipt.amount }} {{ receipt.currency }} on {{ receipt.date }}. Submitted by {{ input.from }}.
If you prefer to reply to the sender instead of (or in addition to) Slack, add a Send Email node and address it to {{ input.replyTo }}, since the Mailhook trigger itself never responds to the sender. Save the workflow.
Tips
- Keep the Response Schema in Step 4 tight. Listing only
vendor,amount,currency, anddateas required gives the most reliable extraction; add optional fields like tax or line items only once the core flow is solid. - Use a
statusfield on the inserted document (for examplecaptured) so a separate review workflow can laterfind-documentson theexpensescollection and pick up new records. - If you want a person to approve large expenses before they are filed, add a Condition node that checks whether
{{ receipt.amount }}is over a threshold and route the true branch through a Human node before the MongoDB insert. - Ask Miraxa, the intelligent layer across your automation, to scaffold the canvas with a prompt like: "Build a workflow that watches a mailhook, fetches the image attachment, OCRs it into a transient Knowledge collection, extracts vendor and amount as JSON, and inserts it into MongoDB."
Common Pitfalls
- Adding an Attachment node without a Mailhook trigger: the designer refuses to save it. The Attachment node only works in Mailhook workflows.
- Choosing a wrong Document Type in the Embed step. A receipt photo must use Images via OCR; picking PDF or Plain Text will not read the picture.
- Mismatched embedding behavior between the two Knowledge nodes. Keep both on Transient so the query reads the document embedded earlier in the same run; a persistent collection in one and transient in the other will not see each other.
- Skipping the Response Schema. Without it the query returns prose, and the MongoDB document will contain a paragraph instead of a numeric
amount, which breaks reporting later. - Forgetting that the Mailhook trigger does not reply to the sender. If staff expect a confirmation, you must add a Slack message or a Send Email step explicitly.
Testing
Before sharing the address widely, email a single clear receipt photo to your Mailhook address from an allowlisted account. Open the run in execution history and confirm each step: the Attachment node shows the image filename and a non-zero size, the Embed step reports a chunk count, and the Query step returns a receipt object with sensible vendor, amount, and date values. Check that one document landed in the expenses collection in MongoDB and that the Slack confirmation appeared. Once a handful of varied receipts (different vendors, currencies, and image quality) come through cleanly, point your forwarding rule at the address and roll it out to the team.