Reference
API reference
The endpoint-by-endpoint contract for the Partner API: every route, parameter, request body, response shape and status code, with a copy-ready example on each endpoint. This page carries what all of them share.
The endpoint reference 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.
The base URL
Examples use the placeholder base URL https://api.visena.example and the instance name acme; replace both with the values you received during onboarding. Every business path starts with the instance name, then the API version, then a locale, then the resource:
Resource names are singular, and the path carries no partner/ segment: it is /{instanceName}/api/v1/{locale}/person, never /partner/api/v1/en/persons. Every endpoint in the reference is written out with those two leading path parameters in place.
The token endpoint is the one exception, because it is not a business endpoint: it lives at /{instanceName}/auth/api/v1/token — the instance name, then the fixed auth surface, and no locale segment.
Resources
Nine resources make up the surface today. Each has its own page with an endpoint list, the full contract of every endpoint, and the object shapes those endpoints exchange.
Person →
Contacts, employees and user accounts. Full CRUD, a duplicate probe, a planned-deletion date, and a feature-gated GDPR erasure.
Read and writeCompany →
Full CRUD and a duplicate probe. An organisation number is unique per instance, and delete deactivates by default.
Read onlyCompany template →
List and fetch. Reference the template from company create and update bodies.
Read and writeProject →
Full CRUD. Create requires a templateId, and delete closes the project instead of destroying it.
Project template →
List and fetch. A project's template decides its type and the classifications available to it.
Read and writeActivity →
The work items on a project. Full CRUD; delete closes by default and can be asked to remove permanently.
Read and writeDocument →
Multipart upload and download, metadata edits, content replacement, and a move between folders.
Read and writeDocument folder →
The folder tree under an owning entity: create, list, rename, move, zip download, and a delete that acknowledges its cascade.
Read onlyDocument changes →
The change feed behind document synchronisation: recorded document and folder changes, cursor-paginated.
Reading this reference
Every resource page is laid out the same way, so the second one you read needs no orientation:
- An endpoint list at the top — method, path and summary, each linking down to the endpoint.
- One section per endpoint: the method and full path, what it does, its parameters, its request body, a table of the responses it can give, and a curl example.
- Object shapes at the foot: every request and response body the page refers to, field by field.
Endpoint anchors are stable. The id of a section is the operation id from the contract — #createPerson, #listPersonsByCursor — and it is the same in both language trees, so a deep link you paste into a ticket keeps working.
What the endpoint pages deliberately leave out: the two leading path parameters, which are on this page; and the four shared errors, which are on Errors and troubleshooting. For worked examples, integration patterns and the guided introduction, read the guide first — Getting started is the short path from a credential to a first response.
The same surface is available as an OpenAPI 3.0 document for import into Postman, Insomnia or a code generator — ask your Visena contact for partner-openapi.json.
Authentication
One standard OAuth 2.0 request turns your clientId and clientSecret into a short-lived bearer token, scoped to exactly what you ask for. Every other endpoint in this reference requires that token. How to create the credential in the interface, and a token cache worth copying, are on Credentials and tokens.
Scopes
The scope form field is mandatory — a token request without it is rejected with 400 invalid_scope. You may ask for your credential's full scope set or any subset of it:
| Scope | Allows |
|---|---|
read |
All safe methods — GET reads, lists, downloads and change feeds. |
write |
All mutations — POST, PUT, PATCH, DELETE. |
act-as-user:read |
Safe methods with an on-behalf credential (one issued to act for a specific named user). |
act-as-user:write |
Mutations with an on-behalf credential. Mutation responses carrying an entity body echo the acting user's masked id in actedBy. |
The two families never mix: a plain credential is registered in the read/write family, an on-behalf credential in the act-as-user:* one — and within its family a credential created as Bare lesing («read only») holds the read scope alone. Asking for a scope your credential does not hold returns 400 invalid_scope. Request only what the job needs — an export job has no business holding write.
Token lifecycle
- Tokens are opaque. Never decode or inspect an
access_token; treat it as random text. expires_inis authoritative. Read the lifetime from every response; never hardcode it.- There is no refresh token. When a token expires, run the same request again — the exchange is cheap.
- Cache and reuse. Mint once, reuse until roughly 60 seconds before expiry, then mint again. Never mint per request.
Using the token
Send it on every request as Authorization: Bearer <access_token>. The token only works on the instance it was minted for, and scopes are enforced per request: safe methods need the read scope, mutations the write scope.
Exchange a credential for an access token
POST /{instanceName}/auth/api/v1/token
Standard OAuth 2.0 client_credentials grant. Authenticate with HTTP Basic (clientId as user name, clientSecret as password) or with client_id/client_secret form fields. The scope parameter is mandatory and must be a subset of the scopes the credential holds — the platform never picks a default.
There is no refresh token: when the token expires, run the same request again. Cache the token and reuse it until shortly before expires_in runs out.
Request body
application/x-www-form-urlencoded → object
Responses
| Status | Description |
|---|---|
| 200 | The access token. TokenResponse · application/json |
| 400 | invalid_scope — scope missing or not held by this credential; invalid_request/invalid_grant — malformed body or wrong grant type. |
| 401 | invalid_client — wrong client_id/client_secret, or the credential was revoked. |
This endpoint does not answer with a problem document. It fails the OAuth 2.0 way, with an error code and an error_description — see Errors and troubleshooting.
Example
curl -X POST https://api.visena.example/acme/auth/api/v1/token \
-u "acme-partner-3f9a:$CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=read write"
{
"access_token": "PhVYGf3qK9t2Zx0CmW8rNbL5aTuJdEwSyAoQiHkXgMfDzUcRvBnOl46E…",
"scope": "read write",
"token_type": "Bearer",
"expires_in": 86399
}
Object shapes
The bodies the token exchange returns. Every other object shape is documented on the resource page that uses it.
TokenResponse
| Field | Type | Description |
|---|---|---|
access_token required |
string |
The bearer token. Opaque — send it back verbatim. |
token_type required |
string |
|
expires_in required |
integer |
Seconds until the token expires. Read it from every response; never hardcode a lifetime. |
scope required |
string |
The scopes actually granted, space-separated. |
OAuthError
| Field | Type | Description |
|---|---|---|
error required |
string |
Machine-readable OAuth 2.0 error code, e.g. invalid_client, invalid_scope. |
error_description |
string |
Human-readable explanation. |
URL shape
Everything from here down is shared by every endpoint on every resource page. The resource pages assume it and never repeat it.
Business endpoints live at /{instanceName}/api/v1/{locale}/…, and those two path parameters are the reason no endpoint table lists them. {instanceName} is the customer instance your credential belongs to — a token minted for one instance gets 403 everywhere else, and an instance name that does not exist gets 404 on every endpoint at once. {locale} (en or no) selects the language of human-readable text, most visibly the detail of an error; it never changes field names, formats or behaviour.
Headers
| Header | Value |
|---|---|
Authorization |
Bearer <access_token> — on every request. |
Content-Type |
application/json on POST and PUT · application/merge-patch+json on PATCH · multipart/form-data on file uploads · omit on GET and DELETE. |
Ids, timestamps, fields
- Masked ids. Every id is an opaque string like
xK9mQ2. Store and echo it verbatim; never parse, order or construct one. A malformed id answers 400; a well-formed id that does not resolve answers 404 as a path id, or 422 as a reference inside a body. - Timestamps.
createdandmodifiedare ISO-8601 date-times with an offset. Date-only fields, for exampledueDate, are plainYYYY-MM-DD. - Absent means absent. Optional fields with no value are omitted from response JSON, not sent as
null. - Audit overrides. Create bodies accept optional
created/createdBy, and update bodiesmodified/modifiedBy, to attribute a change when migrating data; the defaults are server-now and the credential's owner. An unresolvable person reference answers 422. actedBy. Mutations made with an on-behalf token echo the acting user's masked id in the response body.
Updates: PUT vs PATCH
PUT replaces the whole record — anything you leave out is treated as empty. PATCH is RFC 7396 JSON Merge Patch and requires Content-Type: application/merge-patch+json; anything else answers 415. A field you include is updated, a field you set to null is cleared, a field you omit is untouched, and an empty body {} is a valid no-op.
Pagination — three read modes
Every collection offers up to three list variants. The contract is identical across resources, so the shape below is the shape you get on /company and /activity too; each resource page adds only its own filters and projections. Which mode fits which job — and how to build a sync that does not lose rows — is on Lists, paging and sync.
| Mode | Path | Paging parameters | Response envelope |
|---|---|---|---|
| Offset | GET /person |
offset (default 0) · limit |
Items, a real total, and navigation links. |
| Cursor | GET /person/cursor |
cursor · limit |
Items, nextCursor/prevCursor, and links. No total — keyset paging skips the count. |
| Timeline | GET /person/timeline |
since · key (created or modified) · cursor · limit |
The cursor envelope, over everything created or modified since the lower bound. |
Omit cursor for the first page; a cursor minted for one key is rejected on another with 400. On person and company, all three variants also take view, which selects the item projection: lean (the default) returns the compact list item, full returns for every row the same body the single-record read returns, and view=full caps limit at 100.
{
"totalItems": 143,
"totalPages": 8,
"page": 0,
"size": 20,
"items": [ { "id": "xK9mQ2", … }, … ],
"links": {
"self": "/acme/api/v1/en/person?offset=0&limit=20",
"first": "/acme/api/v1/en/person?offset=0&limit=20",
"next": "/acme/api/v1/en/person?offset=20&limit=20",
"last": "/acme/api/v1/en/person?offset=140&limit=20"
}
}
{
"items": [ { "id": "xK9mQ2", … }, … ],
"nextCursor": "Qm5PbDQ2RXhLOW1RMnRaeDBDbVc4…",
"prevCursor": "TmJMNWFUdUpkRXdTeUFvUWlIa1hn…",
"links": {
"self": "/acme/api/v1/en/person/cursor?limit=20",
"next": "/acme/api/v1/en/person/cursor?cursor=Qm5PbDQ2RXhLOW1RMnRaeDBDbVc4…&limit=20"
}
}
| Member | What it is |
|---|---|
items |
The rows on this page, in the projection the resource's view parameter selects where it offers one. Cursor and timeline pages are in ascending key order. |
totalItems totalPages page size |
Offset mode only: the total number of matching rows, the number of pages at the current page size, the zero-based index of this page, and the page size actually applied. |
nextCursor prevCursor |
Cursor and timeline modes only. Opaque page cursors; nextCursor is absent on the last page and prevCursor on the first. |
links |
RFC 8288 navigation links in the body: self, next and prev where they apply, plus first and last on the offset endpoint. |
Follow the links, do not build URLs. Each link preserves your filters and is a valid path against the same base URL. A missing next means the last page, and cursors are opaque — never edit one. Documents add a placement-level feed of their own on top of these three: Document changes.
Errors
Any non-2xx from the business API is an RFC 9457 problem document with Content-Type: application/problem+json. Branch on status and type; show detail to people — it is localized by the {locale} segment and names the offending field on a validation failure — but never parse it.
{
"timestamp": "2026-08-21T11:58:21.123Z",
"status": 422,
"type": "urn:visena:validation:illegal-argument",
"errorType": "ILLEGAL_ARGUMENT",
"title": "Unprocessable Entity",
"detail": "companyId 'bQ4wR8' does not resolve to an existing company",
"instance": "/acme/api/v1/en/person"
}
The document member by member, what to do about each status code, and a retry policy worth copying are on Errors and troubleshooting. The token endpoint is the exception described above: it fails the OAuth 2.0 way rather than with a problem document.
Shared errors — every endpoint
The resource pages list only the responses specific to an endpoint. On top of those, every endpoint can answer these four:
| Status | Meaning |
|---|---|
| 401 | Missing, expired or revoked token. Mint a fresh token and retry once. |
| 403 | The token lacks the scope this method requires, or it was minted for another instance. |
| 404 | Unknown instance name in the URL. |
| 500 | Unexpected server error. Retry with exponential backoff, and report the problem body's timestamp if it persists. |
An endpoint may give the same code a narrower meaning — most 404s are about the id in the path rather than the instance name. Where an endpoint documents a code itself, its own response table is the authority; the four above are documented once, in full, under Shared errors.