NORMAN DEVELOPERS
Document upload and OCR API
Upload receipts, follow batch OCR progress, inspect per-file errors, re-extract fields and import processed documents into Norman.
DOCUMENT WORKFLOWS
Upload, extract and reconcile documents
Choose the workflow according to what should happen after the file arrives.
| Goal | Operation | Result |
|---|---|---|
| Store a receipt and extract its fields | POST /companies/{company_pk}/attachments/ | Document ID; OCR runs asynchronously. |
| Import data already extracted by another system | POST /companies/{company_pk}/attachments/structured-import/ | Document and supplied metadata; no OCR or transaction creation. |
| Process a batch into bookkeeping | POST /accounting/transactions/upload-documents/ | Job ID; OCR creates or matches transactions. |
Process a batch and follow its progress
Upload with write_documents and write_transactions. Send up to 20 files per request, at most 10 MB per file and 50 MB in total. The optional cashflowType is EXPENSE or INCOME.
curl --fail-with-body -X POST "https://api.norman.finance/api/v1/accounting/transactions/upload-documents/" \
-H "Authorization: Bearer $NORMAN_API_KEY" \
-F "files=@receipt-one.pdf" \
-F "files=@receipt-two.jpg" \
-F "cashflowType=EXPENSE"HTTP 201 returns jobId. Save it and poll with read_documents and read_transactions, allowing a few seconds between requests. The same workflow is available below /companies/{company_pk}/accounting/transactions/.
export NORMAN_JOB_ID='replace_with_job_uuid'
curl --fail-with-body "https://api.norman.finance/api/v1/accounting/transactions/upload-documents/$NORMAN_JOB_ID/status/" \
-H "Authorization: Bearer $NORMAN_API_KEY"{
"jobId": "job-uuid",
"totalFiles": 2,
"processedFiles": 2,
"failedFiles": 1,
"status": "completed",
"createdTransactions": [
{
"id": "transaction-uuid",
"description": "Office supplies",
"amount": -4999
}
],
"failures": [
{
"fileName": "receipt-two.jpg",
"reason": "no_usable_data"
}
]
}processing means work is ongoing; completed means the batch has finished, including any failed files. A failed job can still contain partial results. Inspect failures before retrying. Reasons include unsupported_file_type, file_too_large, invalid_file, no_usable_data, duplicate_document and processing_error. Files matched to existing transactions need not appear in createdTransactions.
Re-run recognition or correct a cropped area
Use write_documents. An empty request queues a full extraction and returns HTTP 202 with {"queued": true}. This fills missing data; it does not overwrite every existing value. Read the document again for amount, vatRate, brandName and extractedItems. This operation does not return a polling job ID.
curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/attachments/replace_with_document_uuid/re-extract/" \
-H "Authorization: Bearer $NORMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'To correct values recognized from a particular region, send an image crop. This runs synchronously and returns HTTP 200 with document fields. Review the returned values; a successful response alone does not guarantee that new text was recognized. The stored original file is preserved.
curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/attachments/replace_with_document_uuid/re-extract/" \
-H "Authorization: Bearer $NORMAN_API_KEY" \
-F "image=@amount-crop.png"Import an already processed document
Use write_documents and send one file per request. Keep the same externalSource and externalId on retries: an existing active document returns HTTP 200 and created: false; a new one returns HTTP 201. Supplied amounts use minor currency units, so 11900is €119.00 for EUR. This import does not run OCR or create transactions.
curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/attachments/structured-import/" \
-H "Authorization: Bearer $NORMAN_API_KEY" \
-F "file=@supplier-invoice.pdf" \
-F "externalSource=your-erp" \
-F "externalId=SUPPLIER-2026-1042" \
-F "supplier=Acme Studio" \
-F "invoiceNumber=INV-1042" \
-F "invoiceDate=2026-09-10" \
-F "netAmount=10000" \
-F "vatAmount=1900" \
-F "grossAmount=11900" \
-F "currency=EUR" \
-F "documentType=invoice" \
-F "direction=incoming"Link a primary receipt or add supporting files
These actions require write_documents and write_transactions. Linking a primary receipt can update the transaction’s bookkeeping fields and verification state. Metadata corrections with PATCH /attachments/{id}/ also require both permissions because corrections can propagate to linked transactions.
curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/attachments/replace_with_document_uuid/link-transaction/" \
-H "Authorization: Bearer $NORMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transaction": "replace_with_transaction_uuid"
}'Supporting files use a separate endpoint. They are stored alongside the transaction without OCR or replacing its primary receipt. Send repeated multipart filesor a JSON array of existing attachments IDs. Remove a supporting link with DELETE /accounting/transactions/{id}/attachments/{attachment_pk}/; an otherwise unused document is soft-deleted.
curl --fail-with-body -X POST "https://api.norman.finance/api/v1/accounting/transactions/replace_with_transaction_uuid/attachments/" \
-H "Authorization: Bearer $NORMAN_API_KEY" \
-F "files=@delivery-note.pdf"To retrieve a file, use read_documents with GET /attachments/{id}/download/ or /preview/ below the company path. Download returns a temporary url; preview returns downloadUrl and an optional base64 JPEG previewImage. Keep these links private and request a fresh one after expiry.