Visena Documentation
Partner API

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.

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-dataDocumentCreateForm

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
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
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
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 integer
default 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
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
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
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/jsonDocumentUpdateDto

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
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+jsonobject

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
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-dataDocumentContentReplaceForm

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
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/jsonDocumentPlacementDto

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
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 boolean
default 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
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

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)