Documents
Multipart upload, signed-URL download (15-min TTL), link to journal entries.
Endpoints
GET/api/v1/companies/:companyId/documents/:id/download— Get a time-limited signed download URL for a document.POST/api/v1/companies/:companyId/documents— Upload a document to the WORM archive.POST/api/v1/companies/:companyId/documents/:id/link— Link a document to a journal entry.
GET /api/v1/companies/:companyId/documents/:id/download {#get-documents-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
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 {#post-documents-upload}
documents.upload · scope documents:write
Upload a document to the WORM archive.
Multipart upload of a document (PDF / image) 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. 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 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
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 {#post-documents-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. 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.
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.
Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: yes
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"
}
}