Resource
Document
Files attached to a person, company or project. Multipart upload and download, metadata edits, content replacement, and a move between folders.
A document is a stored blob plus the metadata around it: a name that carries the file suffix, a MIME type, a byte size, an optional free-text description, and a placement in the owning record's folder tree. Every document belongs to an owning entity — a Company, a Person or a Project — and entityType+entityId identify it. There is no organisation-wide document list: every read is scoped to one owner.
There is no content-free create. POST /document is a multipart/form-data upload that writes the blob and the relation in one call, and PUT /document/{documentId}/content replaces the blob in place. The metadata endpoints (replace, patch) never touch the bytes, and the bytes come back only from GET /document/{documentId}/content, which streams them as an attachment rather than as JSON.
Placement and ownership are different things. PUT /document/{documentId}/folder moves a document between folders inside its owning entity; nothing here moves a document to another owner. A document may hold several placements — a master copy plus shortcuts on other records — which is why the delete is relation-aware and asks to have a cascade acknowledged before it removes anything.
The endpoint reference below is kept in English in both language trees. It mirrors the API contract verbatim and there is no generator to rebuild it from, so a translated copy would drift from the API the first time an endpoint changes.
Endpoints
Eleven endpoints, in the order they are documented below.
- POST
/document— Upload a document - GET
/document/{documentId}— Get a document - GET
/document/{documentId}/content— Download a document's content - GET
/document— List documents (offset-paginated) - GET
/document/cursor— List documents (cursor-paginated, insert-stable) - GET
/document/timeline— List documents (time-windowed, cursor-paginated) - PUT
/document/{documentId}— Replace a document's metadata - PATCH
/document/{documentId}— Partially update a document (RFC 7396 JSON Merge Patch) - PUT
/document/{documentId}/content— Replace a document's content - PUT
/document/{documentId}/folder— Move a document to another folder - DELETE
/document/{documentId}— Delete a document (relation-aware)
Every endpoint on this page can additionally answer the shared errors — 401, 403, 404 (unknown instance) and 500. They are documented once, under Errors and troubleshooting.
Two neighbouring route families are documented on their own pages: the six /document/folder endpoints that build and tear down the folder tree are on Document folder, and the recorded change feed — the only read path on which a deletion is observable — is on Document changes.
Upload a document
POST /{instanceName}/api/v1/{locale}/document
Multipart create: writes the uploaded blob and relates it to the resolved owning entity (entityType/entityId). 201 with a Location header. There is no content-free create.
onConflict is applied when a folderId is supplied and that folder already holds a document of the same name: skip leaves the existing document untouched and returns it, creating nothing; replace (the default) overwrites the existing document's content in place, preserving its id and its placements; keepAll creates a second document under a name de-duplicated against the names already in that folder. With no folderId, or no same-name document, all three behave alike. Concurrent creates of the same name in one folder are not serialised — a rare duplicate is possible.
created (ISO-8601) and createdBy (a masked person id) set the creation attribution of a restored document; omitting either defaults it to now / the calling credential's person. Both apply ONLY when a document is actually created: the skip and replace conflict branches create no document and ignore both values. A backdated created also backdates the document's position on GET /document/timeline, so it can land behind a watermark a consumer has already passed and never be seen there. The supplied created is likewise carried as the change timestamp of the recorded creation event on GET /document/changes, so that event is backdated too; only the feed's ordering is unaffected, because that ordering is the insertion sequence and never the timestamp, which is why the feed's cursor is on insertion order and its reported changed timestamp is never a resumption watermark.
Request body
multipart/form-data → DocumentCreateForm
Multipart form: the file part is the uploaded blob; the remaining parts are the non-file create fields.
Responses
| Status | Description |
|---|---|
| 201 | Document created. DocumentResponseDto · application/json |
| 400 | Missing/empty file, unrecognised entityType/onConflict, or the id is not a valid id. |
| 422 | The referenced entityId does not exist for the given entityType, or the supplied createdBy is not an existing person. |
Example
curl -X POST "https://api.visena.example/acme/api/v1/en/document" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@contract.pdf" \
-F "entityType=Company" \
-F "entityId=bQ4wR8"
Get a document
GET /{instanceName}/api/v1/{locale}/document/{documentId}
Returns the full entity. 404 if unknown; 400 if the id cannot be unmasked.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
Responses
| Status | Description |
|---|---|
| 200 | The document. DocumentResponseDto · application/json |
| 400 | The id is not a valid id. |
| 404 | No document with that id. |
Example
curl "https://api.visena.example/acme/api/v1/en/document/dN3pT7" \
-H "Authorization: Bearer $TOKEN"
Download a document's content
GET /{instanceName}/api/v1/{locale}/document/{documentId}/content
Streams the stored blob as an attachment. Content-Type is the document's stored MIME type, falling back to application/octet-stream when it was never recorded or is unparseable; the file name is carried in Content-Disposition (RFC 5987-encoded), not in the path. Same authorization boundary as getDocument — a caller who cannot read the metadata cannot read the bytes.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
Responses
| Status | Description |
|---|---|
| 200 | The document's content, streamed.file · application/octet-stream |
| 400 | The id is not a valid id. |
| 404 | No document with that id. |
Example
curl "https://api.visena.example/acme/api/v1/en/document/dN3pT7/content" \
-H "Authorization: Bearer $TOKEN"
List documents (offset-paginated)
GET /{instanceName}/api/v1/{locale}/document
Offset/limit paging with a real total. entityType+entityId are REQUIRED (documents are always listed scoped to one owning entity — no org-wide enumeration); optional query (full-text search); and a name/created/modified/modifiedOrCreated sort. RFC 8288 navigation links are in the body.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
offset |
query | integerdefault 0 |
Zero-based item offset. |
limit |
query | integer |
Maximum items to return per page. |
query |
query | string |
Free-text filter; the endpoint description lists the fields it matches. |
entityType required |
query | string, one of Company Person Project |
Owning entity type. |
entityId required |
query | string |
Masked id of the owning record. |
sort |
query | string |
Sort key; the endpoint description lists the allowed values. |
Responses
| Status | Description |
|---|---|
| 200 | A page of documents with totals and navigation links. DocumentOffsetListResponse · application/json |
| 400 | Missing/unresolved required entityType/entityId, invalid paging or sort parameter, or a filter id is not a valid id. |
Example
curl "https://api.visena.example/acme/api/v1/en/document?entityType=Company&entityId=bQ4wR8" \
-H "Authorization: Bearer $TOKEN"
List documents (cursor-paginated, insert-stable)
GET /{instanceName}/api/v1/{locale}/document/cursor
Keyset paging over the monotonic entity id (insertion order). Pass the opaque cursor from a previous page's nextCursor/prevCursor; omit it for the first page. No total — keyset skips the count. entityType+entityId are REQUIRED (documents are always listed scoped to one owning entity); optional query.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
cursor |
query | string |
Opaque page cursor from the previous response's links.next; omit for the first page. |
limit |
query | integer |
Maximum items to return per page. |
query |
query | string |
Free-text filter; the endpoint description lists the fields it matches. |
entityType required |
query | string, one of Company Person Project |
Owning entity type. |
entityId required |
query | string |
Masked id of the owning record. |
Responses
| Status | Description |
|---|---|
| 200 | A page of documents with next/prev cursors and links. DocumentCursorListResponse · application/json |
| 400 | Missing/unresolved required entityType/entityId, invalid or expired cursor, invalid limit, or a filter id is not a valid id. |
Example
curl "https://api.visena.example/acme/api/v1/en/document/cursor?entityType=Company&entityId=bQ4wR8&limit=100" \
-H "Authorization: Bearer $TOKEN"
List documents (time-windowed, cursor-paginated)
GET /{instanceName}/api/v1/{locale}/document/timeline
Keyset paging from an optional since lower bound on the single timeline key coalesce(modified, created) — Document has no client-selectable key, unlike Company/Project. entityType+entityId are REQUIRED (documents are always listed scoped to one owning entity). RFC 9557 Z timestamps. A cursor minted by the plain cursor endpoint is rejected with 400.
Sync contract: this feed is upsert-only and at-least-once, ordered on coalesce(modified, created), so creation, archival and renaming are the only writes it reports — renaming is the sole partner-reachable write the platform stamps modified on. Folder moves, content replacement, description-only PATCHes and folder renames/reparents are unreported: they leave the row in place. Deletions never appear here at all and are observable only via GET /document/changes.
Consumer contract: since is inclusive, and cursor timestamps are millisecond-truncated relative to the stored precision, so a boundary row is re-served rather than skipped — overlap the window and de-duplicate by document id. A cross-owner relocation is never a move: it shows only its gaining side here (as a creation in the gaining owner's timeline), while the losing side is visible only as a deletion on GET /document/changes. Full synchronisation therefore needs three sources — this timeline for upserts, GET /document/changes for deletions, and a periodic per-owner snapshot diff for placement, content and description changes. That snapshot diff is the consumer's obligation, not a feature of this API.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
since |
query | string (date-time) |
Inclusive lower bound — an RFC 3339 timestamp. |
cursor |
query | string |
Opaque page cursor from the previous response's links.next; omit for the first page. |
limit |
query | integer |
Maximum items to return per page. |
query |
query | string |
Free-text filter; the endpoint description lists the fields it matches. |
entityType required |
query | string, one of Company Person Project |
Owning entity type. |
entityId required |
query | string |
Masked id of the owning record. |
Responses
| Status | Description |
|---|---|
| 200 | A page of documents with next/prev cursors and links. DocumentCursorListResponse · application/json |
| 400 | Missing/unresolved required entityType/entityId, invalid timestamp, cursor, limit, or a filter id is not a valid id. |
Example
curl "https://api.visena.example/acme/api/v1/en/document/timeline?entityType=Company&entityId=bQ4wR8&since=2026-08-20T02:00:00Z" \
-H "Authorization: Bearer $TOKEN"
Replace a document's metadata
PUT /{instanceName}/api/v1/{locale}/document/{documentId}
Full replace (PUT) of name/description: an omitted description clears the stored value.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
Request body
application/json → DocumentUpdateDto
Responses
| Status | Description |
|---|---|
| 200 | The replaced document. DocumentResponseDto · application/json |
| 400 | Request body failed validation, or the id is not a valid id. |
| 404 | No document with that id. |
Example
curl -X PUT "https://api.visena.example/acme/api/v1/en/document/dN3pT7" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "contract-signed.pdf",
"description": "Signed version"
}'
Partially update a document (RFC 7396 JSON Merge Patch)
PATCH /{instanceName}/api/v1/{locale}/document/{documentId}
Absent fields are unchanged, JSON null clears (name cannot be cleared), a value sets. An empty body {} is a no-op. Wrong content type → 415.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
Request body
application/merge-patch+json → object
Responses
| Status | Description |
|---|---|
| 200 | The updated document. DocumentResponseDto · application/json |
| 400 | A patched field had an invalid value (e.g. blank/cleared name), or the id is not a valid id. |
| 404 | No document with that id. |
| 415 | Content-Type was not application/merge-patch+json. |
Example
curl -X PATCH "https://api.visena.example/acme/api/v1/en/document/dN3pT7" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{
"description": "Signed version"
}'
Replace a document's content
PUT /{instanceName}/api/v1/{locale}/document/{documentId}/content
Multipart PUT: rewrites the stored blob in place, preserving the document id and its relations.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
Request body
multipart/form-data → DocumentContentReplaceForm
Multipart form: the file part is the replacement blob.
Responses
| Status | Description |
|---|---|
| 200 | The document, with the replaced content. DocumentResponseDto · application/json |
| 400 | Missing/empty file, or the id is not a valid id. |
| 404 | No document with that id. |
Example
curl -X PUT "https://api.visena.example/acme/api/v1/en/document/dN3pT7/content" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@contract-v2.pdf"
Move a document to another folder
PUT /{instanceName}/api/v1/{locale}/document/{documentId}/folder
Changes this document's folder placement within its owning entity. A null folderId places it at top level. The destination must belong to the same owning entity — moving a document to another entity is not a move; archive a copy or a shortcut instead. Both the document and the destination folder are resolved only within the declared entity, so either one belonging to another entity is reported as 404 rather than confirming that the id exists elsewhere. A destination that already holds a placement of the same document is rejected with 409. Moving does not count as a modification: modified/modifiedBy are left exactly as the platform recorded them.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
entityType required |
query | string, one of Company Person Project |
Owning entity type. |
entityId required |
query | string |
Masked id of the owning record. |
Request body
application/json → DocumentPlacementDto
Responses
| Status | Description |
|---|---|
| 200 | The document, with its new placement. DocumentResponseDto · application/json |
| 400 | An id is not a valid id. |
| 404 | No document with that id, or no folder with that folderId. |
| 409 | The destination folder already holds a placement of this document. |
| 422 | The document is not archived in a folder, so there is no placement to move. |
Example
curl -X PUT "https://api.visena.example/acme/api/v1/en/document/dN3pT7/folder" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"folderId": "fR5sK1"
}'
Delete a document (relation-aware)
DELETE /{instanceName}/api/v1/{locale}/document/{documentId}
Hard delete: no soft-delete/isActive column. When entityType+entityId are supplied, only that relation is removed; otherwise every relation is removed. The document itself is deleted once it is left orphaned (no remaining relations). 204 on success.
A delete that would cascade — no entityType/entityId, on a document holding shortcut placements on other entities — is refused with 409 until acknowledged, and deletes NOTHING when refused. The 409 states how many master documents and shortcut placements would go; re-send with acknowledgeCascade=true to proceed. The targeted (entityType+entityId) form removes exactly one placement, cannot cascade, and is never gated.
Parameters
| Parameter | In | Type | Description |
|---|---|---|---|
documentId required |
path | string |
Masked document id. |
entityType |
query | string, one of Company Person Project |
Owning entity type. |
entityId |
query | string |
Masked id of the owning record. |
acknowledgeCascade |
query | booleandefault false |
Confirm a delete whose extent goes beyond the targeted placement; see the endpoint description. |
Responses
| Status | Description |
|---|---|
| 204 | The relation(s) — and the document, if orphaned — were removed; no body. |
| 400 | entityType and entityId were not both provided together, or an id is not a valid id. |
| 404 | No document with that id, or no matching relation. |
| 409 | The delete would cascade to shortcut placements and was not acknowledged; nothing was deleted. detail states how many placements would go. |
Example
curl -X DELETE "https://api.visena.example/acme/api/v1/en/document/dN3pT7" \
-H "Authorization: Bearer $TOKEN"
Object shapes
The request and response bodies used above, in the order they are first referenced.
DocumentCreateForm
| Field | Type | Description |
|---|---|---|
file required |
file |
The uploaded file content. |
entityType required |
string, one of Company Person Project |
|
entityId required |
string (masked id) |
Masked id of the owning record. |
folderId |
string (masked id) |
Target folder within the owner's archive. Optional; without it the document is related to the owner but not archived into a folder. |
onConflict |
string, one of replace skip keepAll |
|
created |
string (date-time) |
Audit override: creation timestamp to attribute (ISO-8601). Applied only when a document is actually created. |
createdBy |
string (masked id) |
Audit override: masked person id to attribute the creation to. Applied only when a document is actually created. |
DocumentResponseDto
| Field | Type | Description |
|---|---|---|
id |
string (masked id) |
|
name |
string |
Document display name (includes the file suffix). |
suffix |
string |
File suffix (extension), if any. |
mimeType |
string |
MIME type of the stored blob, if known. |
size |
integer |
Blob size in bytes. |
created |
string (date-time) |
Creation timestamp (UTC). |
createdBy |
PersonRefDto | |
modified |
string (date-time) |
Last-modification timestamp (UTC); null until first modified. |
modifiedBy |
PersonRefDto | |
description |
string |
Free-text description, if set. |
folderId |
string (masked id) |
|
folderPath read-only |
string |
Slash-joined folder path of the placement; empty at top level. |
actedBy |
string (masked id) |
PersonRefDto
Last modifier of the document, if any.
| Field | Type | Description |
|---|---|---|
id |
string (masked id) |
|
name |
string |
Person display name. |
DocumentOffsetListResponse
| Field | Type | Description |
|---|---|---|
totalItems |
integer |
Total number of matching documents. |
totalPages |
integer |
Total number of pages at the current page size. |
page |
integer |
Zero-based index of the current page. |
size |
object |
Page size actually applied. |
items |
array of DocumentListItemDto | The documents on this page. |
links |
DocumentListLinks |
DocumentListItemDto
The documents on this page, in ascending key order.
| Field | Type | Description |
|---|---|---|
id |
string (masked id) |
|
name |
string |
Document display name (includes the file suffix). |
suffix |
string |
File suffix (extension), if any. |
mimeType |
string |
MIME type of the stored blob, if known. |
size |
integer |
Blob size in bytes. |
created |
string (date-time) |
Creation timestamp (UTC). |
createdBy |
PersonRefDto | |
modified |
string (date-time) |
Last-modification timestamp (UTC); null until first modified. |
modifiedBy |
PersonRefDto |
DocumentListLinks
RFC 8288 navigation links: self, and next when more events remain.
| Field | Type | Description |
|---|---|---|
self |
string |
URL of the current page. |
first |
string |
URL of the first page (offset endpoint). |
prev |
string |
URL of the previous page, if any. |
next |
string |
URL of the next page, if any. |
last |
string |
URL of the last page (offset endpoint). |
DocumentCursorListResponse
| Field | Type | Description |
|---|---|---|
items |
array of DocumentListItemDto | The documents on this page, in ascending key order. |
nextCursor |
string |
Opaque cursor for the next page; absent on the last page. |
prevCursor |
string |
Opaque cursor for the previous page; absent on the first page. |
links |
DocumentListLinks |
DocumentUpdateDto
| Field | Type | Description |
|---|---|---|
name required |
string |
Document display name. Required. |
description |
string |
Free-text description. Omitted on a replace clears the stored value. |
DocumentContentReplaceForm
| Field | Type | Description |
|---|---|---|
file required |
file |
The replacement file content. |
onConflict |
string, one of replace skip keepAll |
DocumentPlacementDto
| Field | Type | Description |
|---|---|---|
folderId |
string (masked id) |