Launched this week
space ocr turns photos of receipts, invoices and forms into a table you can query. Drop them into a folder in the app or send them to the API, and each page becomes a row you can filter and sort. Every value shows where it came from. 100 free pages a month.










Hi Product Hunt,
I made space ocr because I kept not trusting OCR output.
Reading a document is the easy part now. Knowing whether the number you got back is the number actually printed on the paper is not. If you still have to open the image and check by hand, you haven't really automated anything.
There are two ways in, and both run the same pipeline.
If you don't want to write code, you upload photos into a folder and they become a sheet. Hover any cell and the photo beside it lights up on the exact spot that value was read from, zoomed in, so checking a page takes a second instead of a squint. Cells that failed the check are marked, so you know which ones to look at rather than rereading all of them. Fix a value by hand and your correction sticks. Folders, memos and search across everything you have scanned are in there too.
If you do write code, three endpoints give you structured fields, markdown, or plain text, and all of them come back with the same verification data: where each value sits on the page, whether it passed the check, and what still needs a look.
Either way the results stay somewhere you can use, so there is no database to stand up. A folder and a sheet are the storage. Photos land in the sheet as rows of the columns you asked for, and later you can ask that sheet for the rows over an amount, or from one vendor, newest first, a page at a time. That runs on the server, it does not read the images again, and it is not charged. The rows keep the coordinates and the flags they were stored with, so a filtered answer is as checkable as a single scan.
If you would rather have an agent do the filing, there is a hosted MCP server on the same account. You point an MCP client at one URL with your key and it can make the folders and sheets, upload photos into them, and ask for rows later. Deleting is the one thing it cannot do in one step. The first call removes nothing and reports what would go, so it has to come back to you before anything disappears.
The checking itself is the part I care about. The model never produces coordinates. Every value it returns is matched character by character against what the OCR engine actually saw on the page. Values that fail get flagged instead of quietly passing, and the ones it still isn't sure about are cropped out of the image and read a second time.
I measured this on my own regression corpus, 333 hand graded cells from phone photos rather than flat scans. Turning the checking stages off drops accuracy from 93.7% to 91.3%. They fixed 22 cells and broke none. A value marked unverified turns out to be wrong 6.4 times more often than average, so the flag is worth acting on.
100 pages a month are free and failed scans are never billed. Same price whichever way you use it.
What I would really like to hear: what would make you trust OCR output enough to skip the manual check? That is the part I keep getting wrong.
Yongha
The 333 cell corpus and the 3 in 4 flag precision are unusually honest for a launch page. The number I'd want next is the other direction, of the cells that were actually wrong, how many did the check miss. Precision tells me the flags are worth reading, recall tells me whether I can skip the unflagged rows, and skipping is the whole product. If recall is weak then a self check is worse than no check, because it's the thing that stops people looking.
@asadmalik901 Recall is the worse number. 39 of the 333 cells were problems, meaning the value disagreed with the hand transcription or the coordinate did not spell its own value. text_verified: false landed on 8 cells, 6 of them real, so it caught 6 of the 39. needs_review is wider and gets 10 of the 25 coordinate errors. There is no combined figure across all 39, so those are the pieces.
The check compares a value against what the OCR pass read at that spot, so it only sees the two disagreeing. Wrong column, value never returned, both readings wrong the same way. None of that shows up. There is a fourth I did not expect. Most of the silently wrong cells were anchored to the wrong occurrence of a string that is printed on the page, and re-reading the crop passes those, since it checks what was read and not where.
Half agree on the last sentence. Sold as permission to stop looking it would be worse than nothing, and I try not to sell it that way. It sits under schema validation and business rules, not instead of them. The ablation also shows the reading improving before flags come into it, 22 cells corrected and 0 broken, scored cell by cell. So the claim is smaller than skipping the manual check. It cuts down what you look at.
Recall is the next thing to move and the harder half, since what it misses is where both readings agree. Method and per stage tables at space-ocr.com/articles/measuring-ocr-verification
Asad's recall question got the most useful answer on this page, and I don't think the 6-of-39 is a tuning problem.
If the re-read runs the same model over the same pixels, it inherits the same failure mode. A 7 that got read as a 1 because the glyph is genuinely ambiguous will get read as a 1 again — the second pass isn't independent, so it can only catch noise, not systematic misreads. That would explain a recall floor that prompt work won't move.
The cheapest independent signal on invoices isn't another model, it's arithmetic. Line items × qty should reconcile to the subtotal, and subtotal + tax to the total. When the sum doesn't close, you know at least one cell is wrong without trusting any model to tell you. I spent 19 years building banking apps and that's what we called a control total — it isn't clever, it just doesn't share the OCR's blind spot.
Do you reconcile totals already, or is the check purely model-vs-model today?
@rodrigo_baigorria Model versus model is the one thing it is not, and the distinction matters enough to spend a paragraph on before I concede the rest.
The first check is not a second pass of the model. The model returns values and never coordinates, and each value is matched character by character against what an OCR engine independently detected at that spot. Two systems that do not talk to each other. The crop re-read is a second pass of the same model, and there your argument lands exactly as you wrote it: same weights, same pixels, so it confirms its own reading of an ambiguous glyph and I learn nothing.
The floor is real in the first stage too, for the reason sitting next to yours. Both readers look at the same pixels. A 7 genuinely shaped like a 1 gets read as a 1 by both, they agree, and agreement is what I report as verified. That is the class I told Asad the check cannot see, and prompt work does not move it, because it was never a prompt problem.
To your question: no reconciliation today. The check is entirely per value. Nothing compares values against each other, and I do not produce derived quantities at all, since fields are asked for as printed rather than as calculated.
A control total is the right shape and I have no good reason for its absence beyond not having built it. The pieces are sitting there. Line items come back as an array with quantity and amount as their own boxed cells and the total is its own field, so the sum closes or it does not, and that answer needs no model to be trusted. Right now that arithmetic is yours to run on the rows I hand back. It should be mine.
Nineteen years of banking apps is why you saw it and I did not. I built this against receipts, where there is a total and nothing to reconcile it with.
the provenance-per-value thing is the part I'd actually pay for. most OCR tools give you a confident-looking number and no way to tell if it read the receipt correctly or just guessed something plausible from a smudge. what happens when the self-check disagrees with the first pass - does it flag the cell as low-confidence for a human to glance at, or silently pick whichever answer scored higher internally? for invoices specifically the failure mode that costs money is a confident wrong number, not a missing one.
@galdayan It never overwrites. The value the model returned is the value you get, and the re-read only decides what gets said about it. If the crop confirms it, text_verified goes to true. If it disagrees, the cell keeps its original value and carries crop_mismatch as a review reason. If the crop comes back empty it abstains and writes nothing, since an empty read on a single glyph is usually a lack of context rather than evidence of anything. There is no internal score picking a winner behind your back.
So a disagreement always surfaces. review_summary at the top of the response lists the flagged paths, and in the app those cells are coloured and open next to the region of the photo they were read from, which is the part that makes glancing at one cheap.
Agreed on the failure mode, and it is the one this is built around. Worth knowing the edge: the re-read checks what was read, not where it was read. A value that is correct but anchored to the wrong occurrence of the same string on the page passes it. For an invoice total that is usually harmless, but I would rather you hear it from me.
@yonghahwang The page as a row model is clean for receipts, but a lot of invoices run three or four pages, with line items continuing past the break and the total only on the last page. Does a multi page PDF land as separate rows I'd have to stitch back together, or can the document be the unit with pages underneath it?
@clement_avq In a sheet, each row is one page, and each row can carry an array field whose sub fields you define as the table’s columns. That is where the line items live: the twelve lines printed on a page nest inside that page’s single row, each one with its own box, and every cell inside them boxed too. The table does not get flattened into one string.
The page break is where it gets thinner. The array is scoped to the page, so page two’s items are their own array on page two’s row, and joining them is concatenating in row order rather than reconstructing anything. The total does what you expect, sitting on the last row and empty on the ones before it. A line that is itself split across the break is the case concatenating does not fix.
The unit ends up being a schema choice, and it changes what a row is. Point the sheet’s columns at the line item’s own columns and the rows become the items, stacking down the sheet. Keep the page as the row instead and one sheet holds many invoices, the pages in order, with the items in one array field and vendor, date, invoice number and total as their own columns. Grouping is then just the invoice number, which is on every page anyway.
@yonghahwang Makes sense, thanks. One thing I'd push on: "the invoice number, which is on every page anyway" is doing a lot of work. Continuation pages often don't reprint it, and either way the grouping key is itself an unverified OCR value, so pages can land in the wrong invoice while every individual cell still passes its check. Is there a document level flag for a page that doesn't resolve to a group, or is assembly outside what gets verified?
@clement_avq You are right and I overstated it. Continuation pages often do not reprint the number, and where they do it is a read value like any other, so a page can join the wrong invoice with every cell passing its own check. There is no document level flag for a page that fails to resolve. Assembly sits outside what gets verified.
It fails quietly too, in the way you would mind most. A page that does not print the number comes back with that cell empty, and a value that was never returned is exactly the class the character comparison cannot see, because there is nothing for it to disagree with.
So I would not group on a read value at all. Queries filter on the file name as well as the columns, and the file name comes from the upload rather than from the page, so naming pages for their invoice groups them by something the OCR never touched. That does not verify assembly. It keeps assembly out of OCR's reach, which is the weaker claim and the true one.
If you do group on the read number, ask for boxes on the query and the key cell carries its own flags like any other cell. That tells you whether that one value was confirmed. It tells you nothing about whether the grouping was right.