Menu

Documents

Underlag: multipart upload, list and read metadata, signed-URL download (15-min TTL), link to journal entries, delete an unlinked document.

Endpoints


GET /api/v1/companies/:companyId/documents

documents.list · scope documents:read

List documents in the archive, linked or not, newest upload first.

Returns document metadata (file name, type, size, SHA-256, version, upload source and time) and whether each is the underlag of a verifikat (linked, journal_entry_id). Filter by linked, journal_entry_id or upload date range (uploaded_from/uploaded_to, YYYY-MM-DD, UTC). Current versions only unless current_only=false. Cursor pagination: pass next_cursor back as cursor; null on the last page. Never returns file bytes or extracted text.

Use when: You need the documents not yet attached to anything (linked=false) before matching receipts, the documents of one verifikat, or an inventory for a period.

Don't use for: Downloading a file (GET /documents/{id}/download), reading its text (the Arkiv tools), or the inbox work queue with its extracted totals (GET /inbox-items).

Pitfalls

  • linked=false still includes documents pinned to a bank transaction or held by an inbox item: GET /documents/{id} shows what holds one.
  • Dates filter the upload time in UTC, not the invoice date on the document.
  • The page is in data.documents with data.next_cursor; a cursor that no longer decodes starts from the first page.

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no

Query parameters

NameTypeRequiredDescription
linked"true" | "false"notrue = only documents linked to a verifikat, false = only unlinked ones. Default: both.
journal_entry_idstringnoOnly the documents of this verifikat.
uploaded_fromstringnoUploaded on or after this date (UTC).
uploaded_tostringnoUploaded on or before this date (UTC).
current_only"true" | "false"nofalse includes superseded versions. Default true.
cursorstringnonext_cursor from the previous page. Omit for the first page.
limitnumbernoPage size, 1-100 (default 50).

Response fields

NameType
documentsobject[]
next_cursorstring | null

Example response

{
  "data": {
    "documents": [
      {
        "document_id": "4f1c…",
        "file_name": "kvitto-clas-ohlson.pdf",
        "mime_type": "application/pdf",
        "file_size_bytes": 48213,
        "sha256_hash": "9b2e…",
        "version": 1,
        "is_current_version": true,
        "upload_source": "email",
        "linked": false,
        "journal_entry_id": null,
        "journal_entry_line_id": null,
        "created_at": "2026-09-02T07:41:10Z"
      }
    ],
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

GET /api/v1/companies/:companyId/documents/:id

documents.get · scope documents:read

Read one document's metadata and what holds it.

Returns the document's metadata, its version chain (original_id, superseded_by_id), the verifikat it is underlag for, the bank transactions it is pinned to (transaction_ids) and the inbox item it arrived through (inbox_item_id). Metadata only: the file is GET /documents/{id}/download.

Use when: You hold a document_id and need to know whether it can be deleted, detached or linked before acting.

Don't use for: Fetching the file (GET /documents/{id}/download) or listing documents (GET /documents).

Pitfalls

  • An id from another company answers 404 DOC_NOT_FOUND.
  • linked=true means räkenskapsinformation: it can never be deleted, only superseded by a new version.

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no

Response fields

NameTypeDescription
document_idstring
file_namestring
mime_typestring | null
file_size_bytesnumber | null
sha256_hashstringSHA-256 of the stored bytes (the WORM integrity anchor).
versionnumber
is_current_versionboolean
upload_sourcestring | nullfile_upload, camera, email, e_invoice, scan, api or system.
linkedbooleanTrue when the document is the underlag of a verifikat (journal_entry_id set).
journal_entry_idstring | null
journal_entry_line_idstring | null
created_atstringUpload time.
original_idstring | null
superseded_by_idstring | null
digitization_datestring | null
transaction_idsstring[]Bank transactions this document is pinned to.
inbox_item_idstring | nullThe inbox item the document arrived through, if any.

Example response

{
  "data": {
    "document_id": "4f1c…",
    "file_name": "kvitto-clas-ohlson.pdf",
    "mime_type": "application/pdf",
    "file_size_bytes": 48213,
    "sha256_hash": "9b2e…",
    "version": 1,
    "is_current_version": true,
    "upload_source": "email",
    "linked": false,
    "journal_entry_id": null,
    "journal_entry_line_id": null,
    "created_at": "2026-09-02T07:41:10Z",
    "original_id": null,
    "superseded_by_id": null,
    "digitization_date": null,
    "transaction_ids": [],
    "inbox_item_id": "1b2c…"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

GET /api/v1/companies/:companyId/documents/:id/download

documents.download · scope documents:read

Get a time-limited signed download URL for a document.

Returns a Supabase Storage signed URL valid for 15 minutes. The URL itself is the canonical download: fetch it with any HTTP client; no API key needed on the storage host. Verify file integrity client-side against the returned sha256_hash if your workflow requires it.

Use when: You need the bytes of an archived document (e.g. for OCR, attachment to an email, regulatory export). Always re-fetch the URL before each download: old URLs expire.

Don't use for: Persisting the URL anywhere: it expires. Storing the URL in a webhook payload or audit log makes the audit trail dependent on URL state.

Pitfalls

  • The signed URL expires after 15 minutes. Don't cache it beyond the immediate transaction.
  • The URL leaks the Supabase Storage origin; this is benign (the signature alone authorizes the read) but rate-limit any forwarding so you don't reveal the storage layout to untrusted callers.
  • Each call emits a document.accessed event. Polling this endpoint produces audit noise; cache the URL for its full TTL.

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no

Response fields

NameType
idstring
file_namestring
mime_typestring | null
sha256_hashstring
is_current_versionboolean
download_urlstring
expires_in_secondsnumber

Example response

{
  "data": {
    "id": "0e9c…",
    "file_name": "kvitto-2026-05-12.pdf",
    "mime_type": "application/pdf",
    "sha256_hash": "8a7f…",
    "download_url": "https://…supabase.co/storage/v1/object/sign/…",
    "expires_in_seconds": 900
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/documents

documents.upload · scope documents:write

Upload a document to the WORM archive.

Multipart upload of a document (PDF, image or Office file) under the BFL 7 kap retention regime. The bytes are hashed (SHA-256), written to Supabase Storage, and recorded in document_attachments at version=1. Allowed MIME types: application/pdf, image/jpeg, image/png, image/webp, image/heic, image/heif, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/vnd.oasis.opendocument.text, application/vnd.oasis.opendocument.spreadsheet, application/vnd.oasis.opendocument.presentation, application/rtf, text/rtf, text/csv. Max size: 10 MB.

Use when: You have a receipt, invoice scan, or supporting document for a posted verifikation and want it archived for the 7-year BFL retention period. Optionally link to a journal entry at upload time via journal_entry_id.

Don't use for: Updating an existing document (no v1 update endpoint; new versions go through the dashboard). Bulk uploads: call once per file.

Pitfalls

  • Idempotency-Key is mandatory; multipart retries with the same key replay the cached response.
  • Max size 10 MB enforced server-side: DOC_UPLOAD_TOO_LARGE on overrun.
  • Only application/pdf / image/jpeg / image/png / image/webp / image/heic / image/heif / application/vnd.openxmlformats-officedocument.wordprocessingml.document / application/vnd.openxmlformats-officedocument.spreadsheetml.sheet / application/vnd.openxmlformats-officedocument.presentationml.presentation / application/msword / application/vnd.ms-excel / application/vnd.ms-powerpoint / application/vnd.oasis.opendocument.text / application/vnd.oasis.opendocument.spreadsheet / application/vnd.oasis.opendocument.presentation / application/rtf / text/rtf / text/csv accepted: DOC_UPLOAD_UNSUPPORTED_TYPE otherwise.
  • WORM: once linked to a posted journal entry, the document row cannot be modified or deleted (DB trigger). Upload-then-link is reversible (the document exists with journal_entry_id=null until linked); once linked, treat as immutable.
  • Dry-run is not supported on this endpoint: the engine hashes + stores + inserts in one atomic flow.

Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: no

Request body

NameTypeRequired
fileunknownno
upload_source"file_upload" | "camera" | "email" | "api"no
journal_entry_idstringno
journal_entry_line_idstringno

Response fields

NameType
idstring
file_namestring
mime_typestring | null
file_size_bytesnumber
sha256_hashstring
versionnumber
is_current_versionboolean
upload_sourcestring | null
journal_entry_idstring | null
journal_entry_line_idstring | null
created_atstring

Example request

{
  "file": "<binary>",
  "upload_source": "api",
  "journal_entry_id": "a8f1…"
}

Example response

{
  "data": {
    "id": "0e9c…",
    "file_name": "kvitto-2026-05-12.pdf",
    "mime_type": "application/pdf",
    "file_size_bytes": 184320,
    "sha256_hash": "8a7f…",
    "version": 1,
    "is_current_version": true,
    "journal_entry_id": "a8f1…"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/documents/:id/link

documents.link · scope documents:write

Link a document to a journal entry.

Sets journal_entry_id (and optionally journal_entry_line_id) on an existing document. Optionally stamps the originating invoice_inbox_items row as consumed via inbox_item_id. Use this after /documents upload when the link target was unknown at upload time, or to re-link a stray document. Once the target JE is posted, the document row is effectively immutable per BFL 7 kap retention.

Use when: A document was uploaded without a journal_entry_id (e.g. bulk import) and you now want to attach it to a posted verifikation. Pass inbox_item_id when the document came from the invoice inbox so the item is marked resolved in one call.

Don't use for: Unlinking: no v1 unlink endpoint. The dashboard exposes a manual override; v1 keeps the WORM contract by refusing to revert posted-JE links.

Pitfalls

  • Idempotency-Key is mandatory.
  • Both the document and the journal_entry_id must belong to the caller's company. NOT_FOUND on mismatch (enumeration hardening).
  • Re-linking an already-linked document overwrites the previous journal_entry_id: confirm the old target is what you intend to break.
  • inbox_item_id stamping is best-effort: if the stamp fails the document link still succeeds. Use POST /api/v1/companies/:companyId/inbox-items/:id/stamp to stamp independently.

Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: yes

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.

Request body

NameTypeRequired
journal_entry_idstringyes
journal_entry_line_idstringno
inbox_item_idstringno

Response fields

NameType
idstring
journal_entry_idstring
journal_entry_line_idstring | null
file_namestring

Example request

{
  "journal_entry_id": "a8f1…"
}

Example response

{
  "data": {
    "id": "0e9c…",
    "journal_entry_id": "a8f1…",
    "journal_entry_line_id": null,
    "file_name": "kvitto-2026-05-12.pdf"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

DELETE /api/v1/companies/:companyId/documents/:id

documents.delete · scope documents:write

Delete a document that is not linked to any verifikat.

Removes the document row and its stored file. Refused once the document is linked to a journal entry: it is then räkenskapsinformation under BFL 7 kap 2 § and must be kept for 7 years (correct it with a new version instead). The database trigger enforces the same rule. Idempotent. Dry-runnable.

Use when: A duplicate, blank or wrong upload that no verifikat references should go.

Don't use for: Taking a document off a bank transaction (POST /transactions/{id}/detach-document), discarding an inbox item (DELETE /inbox-items/{id}) or anything linked to a verifikat.

Pitfalls

  • A linked document returns 409 DOC_DELETE_LINKED, whatever the verifikat's status.
  • The file is removed from storage too: this cannot be undone.
  • A document pinned to an unbooked bank transaction is not protected by this rule: detach it first if the transaction still needs it.

Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: yes

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.

Response fields

NameType
document_idstring
deletedtrue

Example response

{
  "data": {
    "document_id": "4f1c…",
    "deleted": true
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}