CR Receiptsthe manual

Start here

What this is, and the problem it solves

CareRite has thirty-five American Express accounts, one per facility, and many people spending on each. The statement arrives once a month and names none of them.

Measured on CareRite's own ledger, 402,425 card lines have no document behind them. Of 76,879 of those lines, all of them share twenty posting dates — because the ledger records when the batch was keyed, not when the card was swiped. There is no cardholder on the line, no merchant we can rely on, and no receipt.

So by the time anybody looks, four facts are missing and three of them are unrecoverable:

Who
gone
the statement names nobody
What
gone
a merchant code, not a purpose
When
wrong
the posting date, not the swipe
Where
gone
no location at all

This system captures all four at the moment of the purchase, from the person who made it, while they are still standing in the shop. Then it matches what was captured against the statement when it arrives, and tells somebody exactly which charges still have nothing behind them and who to ask.


The shape of it

Four things happen, in this order, every month.

Cardholder's phone who · what · when · where AMEX statement once a month, after the fact The receipts box public · matches the two Azure blob the permanent image store DocStar by email, as today Chase emails sent by CareRite, not the box photographs imported CareRite pulls CareRite delivers CareRite sends
Everything on the right is CareRite reaching IN. The box in the middle never calls out to CareRite — see Running it.

If you carry a card

Photographing a receipt

Three minutes to set up, about eight seconds per receipt after that.

Setting up, once

  1. Open the invite link you were sent. It works once and expires after two weeks.
  2. Choose a password. Ten characters with a number. You will type this on a phone, so something you can type one-handed beats something clever.
  3. Add it to an authenticator app. On the phone, the button opens your app straight into it. On a computer, point the phone's camera at the square. If you have no authenticator, Microsoft Authenticator and Google Authenticator are both free and either works.
  4. Save the eight recovery codes. Each works once, instead of the six digits, if you lose the phone. They are shown exactly once.
  5. Add the app to your home screen. On an iPhone: the share button, then "Add to Home Screen". On Android it offers by itself. It then opens full screen with no browser bars.
Why two steps to sign in? Because this account can file a purchase against a company card. A stolen password alone must not be enough, and a second factor is the only thing that makes it so.

Registering your card

Under My cards, type the last four digits from the front of the plastic and pick your facility. That is all we want.

Never type a whole card number — not here, not in an email, not in a spreadsheet. The system refuses it if you try, and it refuses it for a reason.

Your claim works immediately. An administrator confirms it afterwards; you do not wait for that.

Taking the photograph

Open the camera and point it at the receipt on a flat surface. It takes the picture itself. You do not press anything.

It is watching three things, and all three show on screen as you move:

Sharpness

Whether the image is in focus. If it will not rise, you are too close — most phones cannot focus nearer than about 10 cm.

Steady

Whether your hands are still. Rest an elbow on something. This is the one that fails most.

Fills frame

Whether the receipt is actually in the picture. Fill the four corners; it is measuring paper, not the table.

When all three hold for about half a second, the corners turn green, a ring fills, and it fires. The phone buzzes. If one is failing, the words at the bottom tell you which — "move closer", "hold still" — so move the phone rather than jabbing the shutter.

There is a manual shutter and an Auto switch if you would rather do it yourself, and a "choose a photo I already took" button that works on any phone, with no camera permission at all.

The four seconds afterwards

The paper is read automatically, so the amount and the shop name usually fill themselves in. Check them — the reader is good, not perfect.

Then write what it was for, in your own words: "soda and paper plates for paint night". The account is suggested from those words and the suggestion says why it chose. Tap it if it is right, pick another if it is not, or leave it — AP will code it.

Press Send it to AP. Done.

What is genuinely required: which card, and an amount. Nothing else blocks you. A receipt with a card and an amount can be found on the statement; one without cannot.

"Asked of me"

If a charge arrives on your card with no receipt behind it, it appears under Asked of me and you get an email. If you still have the paper, photograph it. If you never got one — a vending machine, a parking meter — photograph whatever you have and say so in the note. A note saying "no receipt was given, this was parking at the hospital" is a real answer and AP can work with it.

If you run a facility

Getting your people on

A whole facility at once

  1. People → Download the template. It comes with real facility codes already in it.
  2. Fill in a row per person. Only three columns are required: full_name, email, facility_code.
  3. If you know somebody's card, add card_last4 and the card is created and assigned in the same pass.
  4. Upload it. Nothing is written yet. You get a report: how many are new, how many already exist, and every rejected line with its Excel line number and what to fix.
  5. Press apply. You get an invite link per person — copy them all as a table, or download them as a CSV for a mail merge.
The links are shown once. Only their hashes are stored, so nobody — including an administrator — can read one back later. A fresh link per person takes one click if you lose them.

Re-uploading the same file is harmless. People already on the system are updated, not duplicated, and anybody already signed in keeps their password — a re-upload never invalidates a working logon.

The statement name is worth chasing

If the AMEX export prints a name against a plastic — a "Card Member" column — put it on the card under Cards. It is the only field that ties a statement line to a person without a human looking, and every month you have it is a month of matching that happens by itself.

Cards nobody holds

The overview shows these first and in red, because a charge arriving on a card with no holder cannot be chased at all — there is nobody to ask. Assigning somebody takes two clicks and it is the single highest-value thing on the screen.

If you are in accounts payable

The monthly round

1 · Import the statement

Export the month from the AMEX site as a CSV and drop it on Statement. The column names differ depending on where in their site you exported from, so it maps them by name and shows you what it read as what before anything is saved, with the first twenty lines as it understood them.

Re-importing the same month is harmless — a charge is recognised by card, date, amount and reference, so duplicates are counted and skipped.

2 · Look for matches

On Reconcile, press "Look for matches". It proposes, and confirms by itself only where there was never a choice to make: an exact amount, on the same card, inside the window, with no other receipt that could have been it. One ambiguity anywhere and a person decides.

Every proposal carries the reason in words — "the amount is exact; the same card ••4417; 2 days before posting". Read it. That sentence is the difference between confirming a match and clicking a button.

Why the date is weak evidence here. The ledger's posting date is when the batch was keyed — 76,879 lines share twenty dates. The receipt's own timestamp, captured at the till, is the better clock, and the matcher weights it accordingly.

3 · Code what needs coding

On Receipts, anything still without an account has a dropdown right on the row. A correction made here is marked as an AP correction, so the difference between what the cardholder chose and what was right stays visible.

Watch the worth a look line: it appears when what the person typed and what the paper says disagree — a transposed amount, a different card on the slip. That is the line that catches real errors.

4 · Chase what is left

Chase is every charge with nothing behind it, oldest and largest first, with who to ask already worked out. Select, press ask. CareRite sends the emails from the real mail domain on its next run.

This box does not send email, on purpose. A message about a credit card charge, arriving from a machine that is not CareRite's, is a phishing lesson taught to eight hundred staff at the worst possible moment.

What "coded" is worth

Measured, not claimed. Predicting a GL account from a description scored 95.8% in-sample on CareRite's ledger and 53.4% on a temporal holdout — train on months 1..n, predict month n+1. The first number was leakage. That is why the system suggests and never decides, why it offers nothing rather than a guess when it recognises nothing, and why the reason is always on screen.

If you run the box

Separation of church and state

This box is public. CareRite is not. Nothing about that is accidental and it is the constraint the whole design is built around.

The direction of every connection

CareRite → the box

Always. CareRite pushes the facility list, the chart of accounts and the card list up; pulls receipts and their images down; takes the chase queue and sends it; reports back what reached DocStar.

The box → CareRite

Never. There is no CareRite hostname in this process, no credential for it, and no outbound queue aimed at it. If this box is taken tomorrow the attacker inherits receipt images, a facility list and a chart of accounts — and no door.

The one outbound call the box makes is to Azure, for reading receipts, under CareRite's existing Microsoft agreement. Set OCR_MODE=deferred and even that stops: CareRite reads the images on the inside and pushes the results back up. The box then dials nothing at all.

Tokens

Two, narrow, not one with everything:

node bin/token.mjs new puller receipts:read,chase:read
node bin/token.mjs new pusher reference:write,charges:write,ocr:write

The puller cannot push reference data and the pusher cannot read an image, so a leaked token is half a problem instead of a whole one. Every call either makes is logged on the Plumbing screen with its name, address and row count.

Where the images live

In this box's own Postgres, as bytes — so the whole machine moves to another in one dump. CareRite pulls each image into Azure blob, which is its permanent home under our own key, and acknowledges with the sha256. Only then may the local copy be dropped, after PRUNE_AFTER_DAYS. A row is never pruned on the pull alone — a network error would otherwise lose the only good copy.

Moving it to another machine

bash infra/move.sh out /tmp/receipts-2026-10-20.tar.gz
# on the new box, after infra/install.sh
bash infra/move.sh in /tmp/receipts-2026-10-20.tar.gz

One file: the database, the config and the unit files. Point the Cloudflare tunnel at the new host and it is done. Nothing is stored outside Postgres, which is the whole reason the images are in it.

No Docker, deliberately

The machine this runs on already carries other services, has ufw set to drop on INPUT and FORWARD, and runs coturn on 49152–65535. Installing Docker on it inserts rules ahead of ufw and would quietly undo that firewall. Two systemd units and the host's own Postgres instead.

When it goes wrong

The things that actually happen

What you seeWhat it isWhat to do
The code is refused twice in a row The phone's clock has drifted. The codes are time-based, so a phone two minutes out produces codes that were correct two minutes ago. Turn on automatic date and time on the phone. It fixes itself within a minute.
"The camera was refused" The browser was told no once and remembers. Allow the camera in the site settings, or just use "choose a photo I already took" — it needs no permission and files identically.
Sharpness never rises Too close. Most phones cannot focus nearer than about 10 cm. Move back until the whole receipt fits, then hold still.
"That image has been filed into the document store" The image was pulled to Azure and the local copy pruned. Nothing is lost. Look in DocStar. Pruning is the retention policy working.
A charge sits on the chase list with "nobody on this card" No cardholder is assigned, so there is literally no one to email. Cards → add a holder. Then the chase works.
"That is the same photograph you sent" Identical bytes arrived twice. Both are kept, flagged. Nothing, unless they really are two purchases — then take the second one again rather than resending the first.
Plumbing says "Last pull: never" CareRite's puller is not running, or its token is wrong. Receipts are accumulating safely here meanwhile. Check the timer on the CareRite side; the box cannot and must not do anything about it.
OCR says "unknown" The reader could not be reached. Note that this is NOT "unreadable" — "I could not tell" and "there is nothing there" are different answers. Nothing. The worker retries. A genuine unreadable says so.

Lost phone, no recovery codes

An administrator presses "Lost their phone" on the People screen. That clears the password and the authenticator, signs the person out everywhere, and issues a fresh setup link. Clearing only the second factor would leave a one-factor account, so it does not offer that.

Their receipts and history are untouched by all of it.

CareRite Receipts. Questions about a receipt go to your facility's AP contact; questions about the system go to whoever set you up.