Pull structured data out of invoices, in whatever shape you ask for.
Upload an invoice, describe the JSON you want with a visual schema editor, and get exactly that back. Bring your own AI provider and your own API key — they stay in your browser.
flowchart LR
invoice[Invoice PDF or image] --> processor
schema[Your JSON Schema] --> processor
processor -- uses --> AI[(Gemini / GPT / Claude)]
AI -- extracted JSON --> consumer[Consumer: stdout, a file, your datastore]
Want the store name and a line item for every product on a laptop receipt? Build that shape in the editor and it comes back as JSON, ready to feed wherever you need it.
- Go 1.26+ (developed against 1.26.4)
- Node 20+ (developed against 25.9) — only to build the frontend
- An API key for one of the supported providers
git clone <your-repo-url>
cd invoices-processor-golang
makeThat one command runs npm install, builds the UI, and serves it on
http://127.0.0.1:8080. Open it, pick a provider, paste your API key, and go.
No API key is needed to start the server — you supply it in the browser.
The Makefile is the entry point for everything; you shouldn't need to run go or npm
by hand.
| Target | What it does |
|---|---|
make or make run |
Rebuild the UI if it's stale, then serve on 127.0.0.1:8080 |
make dev |
Vite on :5173 with hot reload, proxying /api to the Go server on :8080 |
make test |
go test ./... plus the frontend tests |
make clean |
Remove cmd/webapp/web/{dist,node_modules} |
Three things it handles so you don't have to:
node_modulesanddistare real file targets.npm installruns only whenpackage.jsonchanges, and the UI rebuilds only when a frontend source file does — so a repeatmakestarts the server immediately..envis sourced into the recipe shell if present. It's read as shell, not Makefile syntax, so quotes andexportprefixes behave the way you'd expect.- One
Ctrl-Cis enough undermake dev— it stops Vite and the Go server together.
cd cmd/webapp/web && npm install && npm run build && cd -
go run ./cmd/webappRun it from the repo root, or point -web at the built assets.
| Provider | Value | Default model | Get a key |
|---|---|---|---|
| Google Gemini | googleai |
gemini-3.1-flash-lite |
https://ai.google.dev |
| OpenAI | openai |
gpt-4.1 |
https://platform.openai.com/api-keys |
| Anthropic Claude | anthropic |
claude-haiku-4-5 |
https://console.anthropic.com |
Leave the model field blank to use the default, or type any model name the provider supports.
Your key is stored in your browser's localStorage and sent with each extraction
request. The server holds a client in memory and never writes your key to disk —
there is no key database to leak.
Two consequences worth understanding:
- The server binds to
127.0.0.1by default. If you change-addrto expose it on a network, your key crosses that connection. Put it behind TLS if you do. - Errors from a provider are passed back to you, and keys are stripped from them. That
strip is verbatim-only: OpenAI echoes a self-masked form (
sk-abcd****wxyz) that won't string-match, so fragments can still appear in the server log and the browser. Both are yours, on your machine, but it's worth knowing.
The schema editor is jsonjoy-builder (MIT) — the same tool behind https://json.ophir.dev. Edit visually or drop into the JSON source view.
- The schema you're editing is remembered across reloads.
- Save named templates to switch between extraction shapes — one for hardware receipts, one for fuel, one for supplier invoices.
- Reset to example puts the starter schema back.
Templates live in your browser too. Nothing is stored server-side.
go run ./cmd/webapp [flags]
-addr string
address to listen on (default "127.0.0.1:8080")
-web string
directory of built frontend assets (default "cmd/webapp/web/dist")
A .env file in the repo root is sourced by make if present, so you can put anything
the Go process should see there.
The UI is a client of one endpoint, so you can script against it directly:
curl -X POST http://127.0.0.1:8080/api/extract \
-F 'invoice=@receipt.pdf' \
-F 'schema={"type":"object","properties":{"total":{"type":"number"}}}' \
-F 'provider=googleai' \
-F 'apiKey=YOUR_KEY' \
-F 'model=' # optionalReturns the extracted JSON. Uploads are capped at 20 MiB.
| Status | Meaning |
|---|---|
200 |
Extracted JSON |
400 |
Bad input: missing file, unusable schema, unknown provider, rejected credentials |
502 |
The model call itself failed |
make dev for frontend work, make test before you push — see
Make targets.
Tests are split by language. Go uses the standard library; the frontend uses Node's
built-in test runner (node --test), so there's no test framework to install. Run the
frontend suite alone with npm test inside cmd/webapp/web.
cmd/webapp/ HTTP server and the React app
main.go routes, upload limits, credential plumbing
web/src/
main.jsx the whole UI
storage.js localStorage read/write
templates.js named-schema list operations
internal/
AI.go genkit client cache, extraction
provider.go provider credentials, model names, redaction
processor.go ties AI output to a consumer
consumer.go where extracted data goes (io.Writer)
Processor writes to a Consumer, which is just an io.Writer — so os.Stdout, a
file, or an HTTP response all work. The web app buffers into one; a CLI could write
straight to stdout.
- OpenAI output is less strictly constrained. Genkit's Gemini and Anthropic plugins support native constrained decoding; the OpenAI plugin doesn't, so the schema becomes a prompt instruction rather than an enforced grammar. Responses are unwrapped from markdown fences before being returned, but expect more variance.
- A bad Anthropic key looks like a missing model. Anthropic resolves model names by
calling its API, so an invalid key surfaces as
model "anthropic/..." not found. The real401is in the server log. GENKIT_ENV=devdoesn't work here. Genkit starts a reflection server per client instance, and this app creates one per credential set, so the second provider fails on the busy port. The server warns at startup if you set it.- In-memory client cache is capped at 16. Genkit leaks a signal registration per instance, so the cache evicts oldest-first rather than growing without bound.
- Single user, no accounts, no server-side persistence.
- Firebase Genkit for Go — provider-agnostic model access
- jsonjoy-builder — visual JSON Schema editor (MIT)
- React 19 + Vite
TBD