Developers

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.

GoalOperationResult
Store a receipt and extract its fieldsPOST /companies/{company_pk}/attachments/Document ID; OCR runs asynchronously.
Import data already extracted by another systemPOST /companies/{company_pk}/attachments/structured-import/Document and supplied metadata; no OCR or transaction creation.
Process a batch into bookkeepingPOST /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.