gnubok

Documents

Multipart upload, signed-URL download (15-min TTL), link to journal entries.

Endpoints


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"
  }
}