Skip to content

API Reference

VERA exposes a REST API on port 4000 (default). All document endpoints require authentication via Hub.

Authentication

POST /api/auth/login

Authenticate via Hub and create a session cookie.

Rate limit: 5 requests/minute.

curl -X POST http://localhost:4000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "password": "secret"}'

Response 200 OK:

{
  "username": "alice",
  "role": "user"
}

Sets a vera_session HTTP-only cookie (24-hour expiry).

Errors:

Status Detail
401 Invalid credentials
503 Hub is unreachable -- check HUB_BASE_URL and HUB_AUTH_API_KEY configuration

POST /api/auth/logout

Clear the current session.

curl -X POST http://localhost:4000/api/auth/logout \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"status": "ok"}

GET /api/auth/status

Check current authentication state.

curl http://localhost:4000/api/auth/status \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK (authenticated):

{
  "authenticated": true,
  "auth_required": true,
  "username": "alice",
  "role": "user"
}

Response 200 OK (not authenticated):

{
  "authenticated": false,
  "auth_required": true
}

GET /api/csrf-token

Issue a CSRF token for state-changing requests.

curl http://localhost:4000/api/csrf-token \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"csrf_token": "abc123..."}

CSRF Protection

All state-changing requests (POST, PUT, PATCH, DELETE) require a CSRF token. Fetch a token from GET /api/csrf-token and include it in the X-CSRF-Token header.

# 1. Get a CSRF token
TOKEN=$(curl -s http://localhost:4000/api/csrf-token \
  -b "vera_session=YOUR_SESSION_COOKIE" | jq -r .csrf_token)

# 2. Use it in a state-changing request
curl -X POST http://localhost:4000/documents/upload \
  -H "X-CSRF-Token: $TOKEN" \
  -F "file=@scan.pdf" \
  -b "vera_session=YOUR_SESSION_COOKIE"

CSRF tokens are single-use and expire after 1 hour. Requests without a valid token return 403 Forbidden.


Documents

POST /documents/upload

Upload a scanned document (image or PDF) for OCR processing.

Rate limit: Configurable via UPLOAD_RATE_LIMIT (default 10/minute).

curl -X POST http://localhost:4000/documents/upload \
  -F "file=@scan.pdf" \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{
  "document_id": "a1b2c3d4",
  "image_url": "/files/a1b2c3d4.png",
  "image_width": 0,
  "image_height": 0,
  "status": "uploaded",
  "page_count": 3,
  "pages": [
    {
      "page_id": "e5f6a7b8",
      "page_index": 0,
      "image_url": "/files/e5f6a7b8.png",
      "status": "uploaded",
      "review_complete": false,
      "version": 1
    }
  ],
  "structured_fields": {},
  "review_complete": false
}

Errors:

Status Detail
413 File exceeds upload size limit
415 Unsupported file type or MIME type
400 PDF has no pages / file failed security scan
503 Background worker or PDF support not available

GET /documents/{document_id}

Retrieve document metadata, pages, and status.

curl http://localhost:4000/documents/{document_id} \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{
  "document_id": "a1b2c3d4",
  "image_url": "/files/a1b2c3d4.png",
  "image_width": 2480,
  "image_height": 3508,
  "status": "ocr_done",
  "page_count": 1,
  "pages": [
    {
      "page_id": "e5f6a7b8",
      "page_index": 0,
      "image_url": "/files/e5f6a7b8.png",
      "image_width": 2480,
      "image_height": 3508,
      "status": "ocr_done",
      "review_complete": false,
      "version": 1
    }
  ],
  "structured_fields": {"vendor": "Acme Corp"},
  "review_complete": false
}

GET /documents/{document_id}/pages/{page_id}

Retrieve a single page with all its OCR tokens.

curl http://localhost:4000/documents/{document_id}/pages/{page_id} \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{
  "document_id": "a1b2c3d4",
  "page_id": "e5f6a7b8",
  "page_index": 0,
  "image_url": "/files/e5f6a7b8.png",
  "image_width": 2480,
  "image_height": 3508,
  "status": "ocr_done",
  "review_complete": false,
  "version": 1,
  "tokens": [
    {
      "id": "tok_001",
      "line_id": "line_0",
      "line_index": 0,
      "token_index": 0,
      "text": "Invoice",
      "confidence": 0.97,
      "confidence_label": "trusted",
      "forced_review": false,
      "bbox": [100, 50, 250, 80],
      "flags": []
    },
    {
      "id": "tok_002",
      "line_id": "line_0",
      "line_index": 0,
      "token_index": 1,
      "text": "Numb3r",
      "confidence": 0.42,
      "confidence_label": "low",
      "forced_review": true,
      "bbox": [260, 50, 400, 80],
      "flags": ["low_confidence"]
    }
  ]
}

POST /documents/{document_id}/validate

Submit corrections and validate a single-page document.

curl -X POST http://localhost:4000/documents/{document_id}/validate \
  -H "Content-Type: application/json" \
  -d '{
    "corrections": [
      {"token_id": "tok_002", "corrected_text": "Number"}
    ],
    "reviewed_token_ids": ["tok_001", "tok_002"],
    "review_complete": true,
    "structured_fields": {"vendor": "Acme Corp", "total": "142.50"}
  }' \
  -b "vera_session=YOUR_SESSION_COOKIE"

Request body:

Field Type Description
corrections CorrectionSchema[] List of {token_id, corrected_text} pairs
reviewed_token_ids string[] IDs of all tokens the reviewer has seen
review_complete bool Set true to finalize validation
structured_fields object \| null Updated structured key-value pairs

Response 200 OK:

{
  "validated_text": "Invoice Number: 12345\nVendor: Acme Corp\nTotal: $142.50",
  "validation_status": "validated",
  "validated_at": "2026-03-17T10:30:00",
  "structured_fields": {"vendor": "Acme Corp", "total": "142.50"}
}

Errors:

Status Detail
404 Document not found
409 Review incomplete (forced-review tokens not reviewed)

POST /documents/{document_id}/pages/{page_id}/validate

Validate a single page within a multi-page document.

curl -X POST http://localhost:4000/documents/{doc_id}/pages/{page_id}/validate \
  -H "Content-Type: application/json" \
  -d '{
    "corrections": [],
    "reviewed_token_ids": ["tok_001"],
    "review_complete": true,
    "structured_fields": null,
    "page_version": 1
  }' \
  -b "vera_session=YOUR_SESSION_COOKIE"

Accepts the same body as document-level validation, plus:

Field Type Description
page_version int Required. Must match current page version for optimistic concurrency

Errors:

Status Detail
400 Page version is required
404 Document or page not found
409 Review incomplete or version conflict

GET /documents/{document_id}/export

Export a validated document in multiple formats, including EU e-invoicing standards.

# JSON (default)
curl "http://localhost:4000/documents/{id}/export"

# CSV
curl "http://localhost:4000/documents/{id}/export?format=csv"

# Plain text
curl "http://localhost:4000/documents/{id}/export?format=txt"

# Factur-X (EN 16931 MINIMUM, CII XML)
curl "http://localhost:4000/documents/{id}/export?format=facturx"

# UBL 2.1 XML
curl "http://localhost:4000/documents/{id}/export?format=ubl"

Query parameters:

Parameter Default Options
format json json, csv, txt, facturx, ubl

Response 200 OK (JSON):

{
  "document_id": "a1b2c3d4",
  "validated_text": "Invoice Number: 12345\nVendor: Acme Corp",
  "structured_fields": {"vendor": "Acme Corp", "total": "142.50"}
}

For facturx and ubl formats, the response is application/xml.

Validation warnings

If required invoice fields are missing (e.g. invoice_number, total), the export still succeeds but returns an X-VERA-Warnings header listing the missing fields. Populate structured fields before export for compliant invoices.

Errors:

Status Detail
404 Document not found
409 Document not validated

Page-level export

Use GET /documents/{doc_id}/pages/{page_id}/export?format=json to export individual pages.


POST /documents/{document_id}/reopen

Reopen a validated, summarized, or exported document for further review. Resets page review flags and transitions the document back to review_in_progress.

curl -X POST http://localhost:4000/documents/{id}/reopen \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"status": "review_in_progress", "document_id": "a1b2c3d4"}

Errors:

Status Detail
404 Document not found
409 Document is not in a reopenable status

GET /documents/{document_id}/summary

Generate an AI summary of a validated document via Ollama.

curl "http://localhost:4000/documents/{id}/summary" \
  -b "vera_session=YOUR_SESSION_COOKIE"

# Override model
curl "http://localhost:4000/documents/{id}/summary?model=llama3.2:3b"

Query parameters:

Parameter Default Description
model (from config) Override the Ollama model

Response 200 OK:

{
  "bullet_summary": [
    "Invoice from Acme Corp dated 2026-03-15",
    "Total amount due: $142.50",
    "Payment terms: Net 30"
  ],
  "structured_fields": {"vendor": "Acme Corp", "total": "142.50", "date": "2026-03-15"},
  "validation_status": "summarized"
}

Errors:

Status Detail
404 Document not found
409 Document not validated

Page-level summary

Use GET /documents/{doc_id}/pages/{page_id}/summary for per-page summaries.


POST /documents/{document_id}/fields

Update structured fields for a document.

curl -X POST http://localhost:4000/documents/{id}/fields \
  -H "Content-Type: application/json" \
  -d '{"structured_fields": {"vendor": "Acme Corp", "total": "142.50"}}' \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"structured_fields": {"vendor": "Acme Corp", "total": "142.50"}}

POST /documents/{document_id}/cancel

Cancel a document that is in uploaded or processing status.

curl -X POST http://localhost:4000/documents/{id}/cancel \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"status": "canceled"}

Errors:

Status Detail
404 Document not found
409 Document is not processing / no active task

GET /documents/{document_id}/audit

Retrieve the audit trail for a document.

curl http://localhost:4000/documents/{id}/audit \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{
  "audit_log": [
    {
      "id": "log_001",
      "event_type": "exported",
      "actor": "local_user",
      "detail": {"format": "json"},
      "created_at": "2026-03-17T10:45:00"
    }
  ]
}

Status & Monitoring

GET /documents/{document_id}/pages/status

Get status summary for all pages in a document.

curl http://localhost:4000/documents/{document_id}/pages/status \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{
  "document_id": "a1b2c3d4",
  "status": "ocr_done",
  "review_complete": false,
  "pages": [
    {
      "page_id": "e5f6a7b8",
      "page_index": 0,
      "status": "ocr_done",
      "review_complete": false,
      "token_count": 142,
      "forced_review_count": 5,
      "updated_at": "2026-03-17T10:00:00",
      "version": 1
    }
  ]
}

GET /documents/{document_id}/status/stream

Server-Sent Events stream for real-time status updates.

curl -N http://localhost:4000/documents/{document_id}/status/stream \
  -b "vera_session=YOUR_SESSION_COOKIE"

Cross-origin SSE

The EventSource browser API does not support custom headers. When the frontend runs on a different origin from the backend, authentication relies on cookies sent with withCredentials: true. Ensure CORS_ORIGINS includes the frontend origin.

Query parameters:

Parameter Default Description
interval 2.0 Polling interval in seconds (minimum 0.5)

GET /health

Health check endpoint. Returns 200 if the backend and database are operational.

curl http://localhost:4000/health

Response 200 OK:

{"status": "ok"}

No authentication required

The /health and /metrics endpoints do not require a session cookie.


GET /metrics

Prometheus metrics endpoint. Returns request count and latency metrics.

curl http://localhost:4000/metrics

Returns text/plain in Prometheus exposition format.


LLM Management

GET /llm/models

List available models from the connected Ollama instance.

curl http://localhost:4000/llm/models \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"models": ["llama3.1", "qwen2.5-coder:14b"]}

GET /llm/health

Check Ollama connectivity.

curl http://localhost:4000/llm/health \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK (reachable):

{"status": "ok", "models": ["llama3.1"]}

Response 503 Service Unavailable (unreachable):

{"status": "unavailable"}

POST /llm/models/pull

Pull a model from the Ollama registry (synchronous — waits for completion). Requires admin role.

curl -X POST http://localhost:4000/llm/models/pull \
  -H "Content-Type: application/json" \
  -d '{"model": "llama3.2:3b"}' \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{"status": "pulled", "model": "llama3.2:3b"}

POST /llm/models/pull/stream

Pull a model with streaming progress (NDJSON, one JSON object per line). Requires admin role.

curl -X POST http://localhost:4000/llm/models/pull/stream \
  -H "Content-Type: application/json" \
  -d '{"model": "llama3.2:3b"}' \
  -b "vera_session=YOUR_SESSION_COOKIE"

Each line is a progress update:

{"status": "pulling manifest"}
{"status": "downloading", "completed": 512000, "total": 2048000}
{"status": "success"}

License

GET /api/license/status

Returns the current license enforcement level from Hub. The frontend uses this to show warnings and block actions when the license is expired.

curl http://localhost:4000/api/license/status \
  -b "vera_session=YOUR_SESSION_COOKIE"

Response 200 OK:

{
  "enforcement_level": "licensed",
  "days_until_expiry": 180,
  "grace_days_remaining": null
}
enforcement_level Meaning
grace No key, 14-day evaluation period
licensed Valid key, more than 30 days to expiry
warning Valid key, expiry within 30 days
soft Expired — uploads and validation blocked
hard Expired ≥30 days — all endpoints blocked