← Work

Document Intelligence API

Role
Sole engineer
Stack
FastAPI, Celery, Redis, PostgreSQL, GPT-4o Vision, React, Docker
Status
Runs locally via Docker Compose
The generated OpenAPI reference showing the five document endpoints and a health check.
The generated OpenAPI reference. Five endpoints, plus a health check.

What it is

A backend service that takes a PDF and returns structured JSON. It renders each page with pdf2image, sends it to GPT-4o Vision, and stores the extracted payload against a job record so a front end can poll for the result.

The problem it solves

Calling a vision model on a document is slow and occasionally fails. If you do it inside the request, the HTTP thread blocks for the length of an external API call you do not control, and a transient error loses the user’s upload. Most naive versions of this service are one endpoint that hangs and then 500s.

How I handled it

  • Celery and Redis. Upload returns 202 and a job ID immediately. The model call happens on a worker, so nothing blocks.
  • SHA-256 deduplication. Every file is hashed on arrival. Re-uploading a document that has already been processed returns the stored result instead of paying OpenAI a second time. On a document pipeline this is the difference between a viable cost model and a bad one.
  • Exponential backoff. Model calls retry on transient failures rather than surfacing a stack trace to the caller.
  • Explicit job states. queued, processing, done, failed. The front end polls every two seconds until it reaches a terminal state, so a failure is a state the UI can render rather than a hang.

The API

  • POST /documents/upload: start an extraction, returns a job ID
  • GET /documents/{job_id}/status: current state
  • GET /documents/{job_id}/result: the extracted JSON
  • GET /documents/: paginated history
  • DELETE /documents/{job_id}: remove a job
The upload dashboard: a drop zone for invoice PDFs and a recent-extractions table.
The front end that consumes it, polling every two seconds until a job reaches a terminal state.

What I would change

The deduplication key is the file hash alone, which means the same document extracted against a different schema would wrongly hit the cache. If this went to production the key would need to include the extraction schema version. It has not bitten me yet, which is exactly why it is worth writing down.