Foodops
All docs

Purchasing · Updated 2026-09-01 · vcbec9f1

Inbox — AI document intake for bills, receipts and deliveries

Audio narration for this article is generated with the weekly docs refresh.

Plain-English summary

Inbox is where paperwork goes in before it becomes a record. You drop a supplier bill, a receipt, a delivery note, a bank statement — as a PDF, a photo, an Excel file or a CSV — and the platform reads it for you: supplier, date, reference, currency, subtotal, tax, total and the line items. It then sits in a queue marked Needs review until a human confirms the numbers. Only when you press the button does it become a real Bill draft, Expense record or Purchase receive.

The point is that nothing posts until you approve it. The AI drafts; you decide. Every read is shown next to the original document so you can check it against the paper, and any value the model was unsure about is flagged for your eyes.

It lives in the back-office under Expenses → Inbox (/expenses/inbox), and it is off unless your organisation has it switched on — see Prerequisites.

When you'd use this

  • A stack of supplier invoices arrived and you don't want to key each one into Bills by hand.
  • A driver dropped a delivery note and you want it turned into a Purchase Receive against the right PO.
  • Staff photograph receipts on their phones and you want them captured as expense records.
  • You want a check on double-paying: the Inbox flags a document that looks like one you've already had.
  • You want an audit trail from a posted bill back to the actual scan it came from.

Prerequisites

  • The expenseInbox feature must be enabled for your organisation. This is the one that catches people out. Every brand — foodops, salesmade and finance — ships with the flag defaulting to off, so the Inbox nav entry and the upload buttons are hidden until someone turns the feature on for your org in the Hubits System Management app. There is no per-user or per-browser override. [code: foodops-admin-app/apps/admin/src/config/brands.ts — foodops/salesmade/finance all hasExpenseInbox: false; apps/admin/src/services/featureFlagService.ts — org toggle via GET /v1/features, else brand default @ main]
  • Bills permission. The Inbox pages are guarded by the same permission as Bills — you need Bills View or Bills Create. [code: apps/admin/src/config/routePermissions.ts:104,202-203 — BILLS_READ = [Bills.View, Bills.Create] on '/expenses/inbox' and '/expenses/inbox/:id' @ main] On the FoodOps brand the sidebar entry additionally requires one of those two roles before it will render. [code: apps/admin/src/config/brands.ts:396 requiredRoles: [ROLES.Bills.View, ROLES.Bills.Create] @ main]

How to access

Route in Where
Sidebar Expenses → Inbox (inbox icon, with a count badge of documents waiting for review) → /expenses/inbox [code: brands.ts:396 label 'Inbox', badgeKind 'documents-review' @ main]
From Bills The Bills list toolbar has an Upload documents button — "Drop a supplier PDF — AI drafts the bill for review" — which opens /expenses/inbox?upload=1 with the file picker already open. Only shown when the feature flag is on. [code: apps/admin/src/pages/expenses/BillsPage.tsx:390-393 @ main]
From Purchase Receives A button on the Purchase Receives list opens /expenses/inbox?view=receives, a receives-scoped lens of the same queue. [code: apps/admin/src/pages/expenses/PurchaseReceivesPage.tsx:536-537 @ main]
Old bookmarks /expenses/documents and /expenses/documents/:id still work — the screen was renamed and the old paths redirect to /expenses/inbox, carrying the document id, the query string and the hash with them. [code: apps/admin/src/App.tsx:44-56 LegacyInboxRedirect, :1571-1581 route registrations @ main]

Walkthrough — turn a supplier invoice into a Bill draft

  1. Open Expenses → Inbox.
  2. Add the file. Drag it anywhere onto the panel (a "Drop to upload" overlay appears) or use the Upload documents button. You can select several files at once. Accepted: PDF, JPG/JPEG, PNG, GIF, WEBP, XLSX, XLS, CSV, up to 10 MB each — a bigger file is refused by name before anything uploads. [code: DocumentsInboxPage.tsx:43-44 ACCEPT / MAX_BYTES = 10_000_000; uploadFiles() rejects the first oversize file @ main]
  3. Each file returns immediately as Processing and the page keeps re-asking the server roughly every 4 seconds until the reads finish, so the row flips to Needs review without you refreshing. [code: DocumentsInboxPage.tsx POLL_MS = 4000 @ main]
  4. Pick the row. The left pane is the queue, the right pane is the selected document. / (or k/j) move through the queue and Enter opens the selected document for review. [code: DocumentsInboxPage.tsx onKeyDown — ArrowUp/ArrowDown/k/j/Home/End/Enter @ main]
  5. Review the reads. The review screen shows the original document beside the extracted header — supplier name, address, phone, document date, due date, reference, PO reference, currency, subtotal, tax, total — and the line items. Anything the model scored below 70% confidence is marked "Low confidence — check this value", which is your prompt to compare it with the paper. [code: inboxFormat.ts LOW_CONFIDENCE = 0.7; BillReviewPanel.tsx ConfidenceChip aria-label @ main]
  6. Fix what's wrong. Edits autosave as you go against a concurrency token, so a colleague working the same document can't silently overwrite you — you're told to reload instead. [code: documentsService.saveReviewState — expectedReviewStateToken, 409 on conflict @ main]
  7. Confirm the supplier. If the extractor read a supplier name it couldn't match to a supplier record, you must pick or create one before it will post — the panel refuses with "Pick the supplier first." [code: BillReviewPanel.tsx:242 @ main]
  8. Post it. Press Create bill draft. The bill is created through the normal Bills endpoint, so it lands as a Draft like any other bill, and the source file is attached to it as provenance. If the attach fails you get a warning telling you to add it from the Files tab. [code: BillReviewPanel.tsx:277,296-304 — markConverted returns sourceFileAttached @ main]
  9. The document's status becomes Converted and its row links through to the bill it became. [code: inboxFormat.ts resultLink → /expenses/bills/{id} @ main]

Walkthrough — a delivery note into a Purchase Receive

  1. Open the Inbox (or come in from Purchase Receives, which opens the receives-scoped view showing delivery notes and priced invoices only).
  2. Select the delivery note. A document the AI classified as a DeliveryNote opens in the receive panel rather than the bill panel.
  3. The PO links itself when it safely can. If the document quotes a PO reference and exactly one open purchase order matches it, the receive is linked to that PO automatically; ambiguity is left for you. Changing the supplier clears the link, because a PO belongs to its supplier. [code: ReceiveReviewPanel.tsx:262-275 matchPoByReference — auto-link only on a single match; :611-616 supplier change clears it @ main]
  4. Check the quantities against the PO lines. PO lines this delivery doesn't cover simply stay outstanding.
  5. Post it — the result is a draft purchase receive, again linked back to the source document.

Key concepts

  • Nothing posts until you approve it. Uploading creates a document, not a bill. The queue is a staging area; conversion is an explicit button press by a human. This is stated on the empty state and on the drop overlay in the product itself.
  • Status is the spine of the queue. A document is Processing (being read) → Needs review (read, waiting for a human) → Converted (became a record) or Dismissed (deliberately not posted), with Failed for a read that didn't work. The tabs are All / Needs review / Processing / Failed / Converted / Dismissed, each with a live count, and All is the default so a document doesn't vanish off your screen the moment its status changes. [code: inboxDocument.ts InboxDocumentStatus; DocumentsInboxPage.tsx STATUS_TABS @ main]
  • Classification vs destination. The AI classifies a document as Bill, ExpenseReceipt, BankStatement, DeliveryNote or Other, and that classification proposes a destination: delivery note → Purchase receive, bill → Bill, anything else → Expense record. You can override the destination, and your override wins over the AI's read from then on — the row chip, the detail pane and the review screen all name the same destination. [code: inboxFormat.ts aiRoute / effectiveRoute; documentsService.setDestination @ main]
  • Two exception flags the queue can actually prove. Possible duplicate (this looks like a document you've already had — the row links to the one it matched) and No supplier (a supplier name was read but matched no supplier record, so it can't post yet). Deliberately not a filter: "low confidence" — the confidence scores live on the document detail, not the list, so the list would be claiming something it can't see. [code: inboxTriage.ts issueOf + its comment @ main]
  • Large-value chip. Documents worth 10,000 or more in your base currency get their own filter, using the org's configured exchange rates to compare a foreign document with a local one. A document whose value can't be converted is never treated as though it were worth zero. [code: inboxTriage.ts BIG_THRESHOLD = 10_000, baseValueOf returns null rather than 0 @ main]
  • Dismiss vs delete. Dismiss requires a reason, keeps the document as a record of a decision not to post it, and is the right move for something like a bank statement. Delete permanently removes the row and the stored file, and the server refuses to delete a document that has already been converted — it is the created record's provenance. [code: documentsService.dismiss / delete + their doc comments; hubits-api-expenses DocumentDecisionTests — Dismiss_requires_a_reason…, Delete_refuses_converted_documents @ main]
  • Bank statements don't post from here. A document classified as a bank statement shows a banner pointing at Banking → Statements and asks you to dismiss the inbox copy once you've imported it there. [code: BillReviewPanel.tsx:404-409 @ main]

Common questions

Q: What is the Inbox in Expenses? A: It's the AI document-intake queue. You drop a supplier bill, receipt, delivery note or statement into it (PDF, image, Excel or CSV, up to 10 MB), the platform reads the supplier, dates, reference, currency, tax and totals plus the line items off it, and it waits in a Needs review list until a human confirms the numbers. Confirming turns it into a Bill draft, an Expense record or a Purchase receive. Nothing posts on its own.

Q: I can't see Inbox in my sidebar — where has it gone? A: The Inbox is behind an organisation feature switch called expenseInbox, and it ships off by default on every brand — FoodOps, Salesmade and Finance alike. Someone with access to the Hubits System Management app has to enable it for your organisation; there's no setting you can flip yourself and no per-user override. You also need Bills View or Bills Create permission for the entry to render at all.

Q: What file types can I upload, and how big? A: PDF, JPG/JPEG, PNG, GIF, WEBP, XLSX, XLS and CSV — up to 10 MB per file. You can drop several at once. A file over the limit is rejected by name before anything is uploaded.

Q: Does uploading a document create a bill straight away? A: No. Uploading creates a document in the queue with status Processing, which becomes Needs review once the read finishes. A bill, expense record or purchase receive is only created when you press the button on the review screen — and the bill arrives as a Draft, exactly like one you'd typed in yourself.

Q: How do I know which numbers the AI wasn't sure about? A: Anything read with less than 70% confidence is flagged on the field with a "check this value" marker. The original document is shown alongside the extracted values, so the check is a glance rather than a hunt.

Q: The document is stuck on "Processing". A: The queue re-asks the server every few seconds while anything is still being read, so a row that has genuinely finished will flip on its own without a refresh. If it lands on Failed instead, the original file is kept — re-scan the paper if the scan was poor and use Retry, which puts it back through the reader.

Q: It read the supplier wrong / didn't find the supplier. A: A document where a supplier name was read but couldn't be matched to a supplier record is flagged No supplier, and it will not post until you pick or create the right supplier — the panel says "Pick the supplier first." Use the No supplier filter to see all of them together.

Q: It says "possible duplicate" — have I been billed twice? A: Not necessarily; it means the platform found an earlier document that looks like this one, and the row links straight to it so you can compare. If it really is a re-send, Dismiss the new copy with a reason rather than deleting it, so the decision stays on the record.

Q: The AI sent it to the wrong place — it's an expense, not a bill. A: Change the destination on the document. Your pick is stored and overrides the AI's classification everywhere the destination is shown, so the row, the pane and the review screen agree. The classification itself is left untouched — it's a record of what the model read.

Q: What's the difference between dismissing and deleting a document? A: Dismiss needs a reason and keeps the document as evidence of a decision not to post it — use it for a duplicate or a bank statement. Delete permanently removes the row and the stored file, and is meant for a mistaken upload. A document that has already been converted cannot be deleted, because it is the created record's provenance.

Q: I have a bookmark to /expenses/documents — is that page gone? A: No, it was renamed. /expenses/documents and /expenses/documents/:id redirect to the matching /expenses/inbox path and carry the document id, query string and hash across, so old links and ?upload=1 / ?view=receives bookmarks still land.

Q: Can I upload a bank statement here? A: You can upload it and the reader will classify it, but it does not post from the Inbox. The review screen points you at Banking → Statements to import it, and asks you to dismiss the inbox copy afterwards.

Q: Can I get delivery notes onto their purchase orders automatically? A: Partly. If the delivery note quotes a PO reference and exactly one open purchase order matches it, the receive links to that PO by itself. When more than one could match, it leaves the choice to you rather than guessing. Changing the supplier clears the link, since a PO belongs to its supplier.

Edge cases and known issues

The feature flag is off, but the URL still opens

  • Symptom: Inbox isn't in the sidebar and the Bills "Upload documents" button is missing, yet typing /expenses/inbox reaches the page.
  • Cause: The flag gates the nav entry and the two upload entry points; the route itself is guarded by the Bills permission, not by the feature flag. [code: App.tsx route has ProtectedRoute + OnboardingGate only; routePermissions.ts guards on BILLS_READ @ main]
  • Note: This is inferred from the route wiring, not live-walked. Whether an org without the feature has a working documents API behind that page is not verified — treat the URL as an unsupported back door, not a workaround.

Two people reviewing the same document

  • Symptom: "Another session saved first" / a reload prompt on the review screen.
  • Cause: Review edits autosave against a concurrency token; a second saver gets a 409 rather than silently overwriting the first.
  • Workaround: Reload the document and re-apply your edits.

A converted document won't delete

  • Symptom: Delete is refused on a document in Converted.
  • Cause: Deliberate — the document is the provenance of the bill/expense/receive it became.
  • Workaround: If the resulting record is wrong, act on that record; the source document stays as the audit trail.

The source file didn't attach to the created bill

  • Symptom: Success toast plus a warning that the file could not be attached.
  • Cause: The record was created but the server couldn't copy the source file onto it.
  • Workaround: Add the file manually from the record's Files tab, as the warning says.

Value filters go quiet without exchange rates

  • Symptom: The large-value filter stops matching foreign-currency documents.
  • Cause: Comparing a foreign document against the 10,000 threshold needs the org's currency rates; without a known base currency nothing is presented as converted.
  • Workaround: Configure currencies and rates in Settings → Accounting → Currencies.

Related

  • Recording Expenses (Foodops) — Bills, Expense Records and Payments Made, the records the Inbox feeds
  • Purchase Orders — the POs a delivery note gets received against
  • Suppliers — the supplier records a document has to match before it can post
  • Settings overview — Currencies and rates, which the value filter depends on
  • Navigation & module map — where Expenses sits in the admin sidebar

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.