Part 2 of 7 · Expense approver series ~4 min read

How an expense claim gets submitted

The approver can only check what it’s been given. So the first job is making it dead simple to hand it a claim, because the easier the submission, the fewer receipts end up lost in a coat pocket. There are three ways a claim gets in: somebody fills a short web form, somebody forwards a receipt email, or somebody drops a photo into a team chat. The point of having three is that people submit differently, and a claim that never gets submitted is the only one the system can’t help with.

Key takeaways

  • Three intake lanes feed one claim record: a web form, a forwarded email, and a chat upload.
  • Each receipt is read by Textract into amount, date, and vendor.
  • Bedrock Haiku 4.5 sorts the receipt into a category so the claimant doesn’t have to be exact.
  • The claimant sees the read-back values and can correct them before the claim is filed.
  • However it arrived, every claim becomes one record in the same shape.

Three lanes into one claim

Three intake lanes funnel into one claim record A diagram with three vertical lane columns at the top and a single unified row at the bottom. Lane one, Web form: a staff member opens a short form behind a Lambda Function URL, picks a category, types an amount, and uploads a receipt photo; the form Lambda writes the receipt to S3 and creates a draft claim. Lane two, Forwarded email: a staff member forwards the vendor's emailed receipt to a dedicated address; SES inbound writes the raw MIME to S3; a parser Lambda pulls the receipt attachment and starts the read step. Lane three, Chat upload: a staff member drops a receipt photo into a team chat channel; a chat-intake Lambda fetches the file and starts the same read step. All three lanes then run the same read-and-sort step: Textract reads the receipt image into amount, date, and vendor, and Bedrock Haiku 4.5 sorts it into a category; the claimant sees the read-back values and can correct any field before the claim is filed. All three converge on the same claim record in DynamoDB. A note at the bottom: however a claim arrives, it becomes one record in the same shape — the lanes are just doors into the same room. Lane 1 · Function URL Web form • Pick category, type amount • Upload a photo of the receipt • Receipt saved to S3 • Draft claim created Lane 2 · SES inbound Forwarded email • Forward receipt to expenses-address • SES writes MIME to S3 • Parser pulls the receipt attachment • Starts the read step Lane 3 · chat event Chat upload • Drop a receipt into a chat channel • chat-intake fetches the file to S3 • Same read step as Lane 2 • Claimant is the person who posted One claim record (read by Textract, sorted by Bedrock) claimant · category · amount · date · vendor · receipt link · status claimant sees the read-back values and can correct any field to checker, on submit However a claim arrives, it becomes one record in the same shape — the lanes are just doors into the same room.
Fig 2. Three lanes converge on one claim record. The web form, the forwarded email, and the chat upload all end with Textract reading the receipt and Bedrock sorting the category. The claimant gets to fix any misread value before the claim goes to the checker.

Lane 1: the web form

The simplest lane, and the one most people use. A short form sits behind a Lambda Function URL — no app to install, just a link the team bookmarks. Pick a category from a short list, type the amount, snap or upload a photo of the receipt, hit submit. The form Lambda writes the receipt image to s3://ea-receipts/ and creates a draft claim in DynamoDB with the claimant’s identity (they signed in), the category they picked, and a pointer to the receipt.

The form is deliberately tiny. Three fields and a photo. Anything the team has to think about is a thing that gets done wrong or skipped, so the form asks for the minimum and lets the read step in a moment fill in the rest.

Lane 2: forwarded receipt email

Half of business receipts arrive as email in the first place — the ride app, the SaaS invoice, the online order confirmation. Forcing the team to download those and re-upload them to a form is busywork. So there’s a dedicated inbound address — something like expenses@your-company.com — set up via Amazon SES. Forward the receipt email to it and the system takes over.

SES writes the raw email to s3://ea-raw-mime/. The S3 PUT triggers a parser Lambda that walks the email, finds the receipt (a PDF attachment, an image, or even the receipt text in the body), and starts the same read step the form uses. The claimant is matched from the “From” address of the forward, so the system knows whose claim it is. If the match is unclear, the claim is held and the system asks the sender to confirm before it goes anywhere.

Lane 3: chat upload

Some teams live in chat. The receipt photo gets dropped into a channel with a one-line “client lunch” before the person even thinks of it as an expense. Lane 3 catches those. A chat-intake Lambda listens for file uploads in a configured expenses channel, fetches the image, writes it to S3, and starts the read step. The person who posted is the claimant.

This lane is the most opt-in of the three. A team that doesn’t use chat for this loses nothing; a team that does gets to submit a claim without leaving the tool they’re already in.

Reading the receipt, once, the same way

Whatever lane a claim came in through, it hits the same read step. Amazon Textract reads the receipt image and returns the printed values — most importantly the total amount, the date, and the vendor name. Textract handles photos, PDFs, and scans natively, so a phone snap of a crumpled receipt works fine. Then a short Bedrock Haiku 4.5 call reads the vendor and line items and sorts the receipt into one of your policy categories: “this looks like meals,” “this looks like a taxi,” “this looks like software.” The category the claimant picked is treated as a hint, not the final word — if the receipt clearly says otherwise, the system flags the mismatch.

The claimant always sees the read-back before the claim is filed: “We read $36.00, today, Olive & Vine, category meals — correct?” They can fix any field with a tap. This matters because a misread amount is the one error you really don’t want flowing into an approval: too low and the claimant is shorted, too high and the business overpays. A human confirming the read keeps both honest.

Why everything becomes one record

Three lanes in, but only one shape of claim record the rest of the system ever sees. That’s deliberate. The checker in the next post, the routing after it, and the audit trail all work on a single claim shape: claimant, category, amount, date, vendor, receipt link, status. The lanes are just convenient doors; once a claim is through any of them, it’s indistinguishable from a claim that came through the others. One shape means one set of rules to reason about and one place to look when somebody asks “what happened to my claim?”

Next post: how the checker reads a claim, sorts it against policy, and picks one of four outcomes.

All posts