Visena Documentation
Partner API

Guide

Making requests

Every call is the same recipe: the instance-prefixed URL, your bearer token, JSON in, JSON out. This page walks one person record through create, read, update and delete, and stops on the one thing worth reading twice — what PUT and PATCH do differently.

Anatomy of a URL

Every business endpoint is built from the same parts in the same order. Nothing has to be discovered at runtime: if you can write the URL, you can call the API.

https://api.visena.example/acme/api/v1/en/person/xK9mQ2
/acmeinstance name /enlocale /personresource

The instance name comes first, which is what lets one integration serve several Visena instances by swapping a single segment. Your token is minted for one instance, so the token and the path have to agree — a mismatch is a 403, not a silent cross-instance read.

The locale (en or no) selects the language of human-readable text in the response, most visibly the detail of an error. It never changes field names, formats or behaviour, so pick one and use it consistently. Then comes the resource, and after it a masked id.

Resources are singular nouns, and the path carries no partner/ segment: it is /{instanceName}/api/v1/{locale}/person. Nine resource paths make up the surface today — person, company, company-template, project, project-template, activity, document, document/folder and document/changes. Each one is documented endpoint by endpoint in the endpoint reference.

Headers on every request

Three headers decide whether a request is understood at all. Two of them are yours to set; the third is worth knowing about because it is how an error announces itself.

Header Value
Authorization Bearer <access_token> — on every call, without exception. Tokens are short-lived and scoped to one instance; see Credentials and tokens.
Content-Type application/json on POST and PUT · application/merge-patch+json on PATCH · multipart/form-data when you upload a document · omit it entirely on GET and DELETE.
Accept Optional. A successful response is application/json; an error is application/problem+json — the RFC 9457 problem document described in Errors and troubleshooting.

Create

POST /{instanceName}/api/v1/{locale}/person

POST /acme/api/v1/en/person
curl -X POST "https://api.visena.example/acme/api/v1/en/person" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Kari",
    "lastName": "Nordmann",
    "primaryEmail": "kari.nordmann@example.com",
    "jobTitle": "Operations Manager"
  }'
HTTP/1.1 201 Created
HTTP/1.1 201 Created
Location: /acme/api/v1/en/person/xK9mQ2

{
  "id": "xK9mQ2",
  "firstName": "Kari",
  "lastName": "Nordmann",
  "primaryEmail": "kari.nordmann@example.com",
  "jobTitle": "Operations Manager",
  "created": "2026-08-21T08:02:44Z",
  "createdBy": "pQ7hL4",
  "modified": null,
  "modifiedBy": null
}

Two things in that response shape the rest of your integration. Location carries the new resource's own path, and id is the masked id you use from here on: store it exactly as received and echo it back verbatim. It is opaque — it carries no ordering, no meaning outside the API, and cannot be constructed. Every other field of the resource is in the body too, and the ones you did not send came back as null; timestamps are RFC 3339, normalised to UTC by the server.

An import can keep its own history: send created, createdBy, modified or modifiedBy and those become the record's attribution instead of «now» and the credential's owner. A createdBy or modifiedBy that does not resolve to a person in the instance is refused with 422, and nothing is written.

i

Probe for duplicates before you create. GET /…/person/duplicates?email=kari.nordmann@example.com (or name plus birth date) returns likely matches, so a sync does not create the same person twice. Companies have the same probe keyed on organisation number — and there it is more than advice: creating a company with an organisation number that already exists is refused with 409.

Read

GET /{instanceName}/api/v1/{locale}/person/{personId}

GET /acme/api/v1/en/person/xK9mQ2
curl "https://api.visena.example/acme/api/v1/en/person/xK9mQ2" \
  -H "Authorization: Bearer $TOKEN"

A read by id returns the full entity, the same body a create or an update returns. An id that does not exist is a 404; a string that is not a masked id at all is a 400, because it fails before any lookup. Reading many records — offset paging, cursor paging and the change feed — is its own subject: see Lists, paging and sync.

Update — two flavours

Both verbs address the same record by id and both answer 200 with the updated entity. They differ in what happens to the fields you do not mention, and that is the difference to settle before you write a sync.

PUT replaces

PUT with Content-Type: application/json is a full replace: every writable field you leave out is cleared. Send the whole record every time. Use it when your system owns the record outright and its copy is the truth.

PUT /acme/api/v1/en/person/xK9mQ2
curl -X PUT "https://api.visena.example/acme/api/v1/en/person/xK9mQ2" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Kari",
    "lastName": "Nordmann",
    "primaryEmail": "kari.nordmann@example.com",
    "jobTitle": "Head of Operations"
  }'

That call sets the job title — and if this person had a directPhone, that number is now gone, because the body did not mention it. created and createdBy are the one exception: they are immutable on update and quietly ignored.

PATCH merges

PATCH is a JSON Merge Patch (RFC 7396), sent as application/merge-patch+json. It changes only what the body mentions, and three rules cover it:

  • A field you include with a value is set to that value.
  • A field you set to null is cleared.
  • A field you omit is left exactly as it was.
PATCH /acme/api/v1/en/person/xK9mQ2
# sets jobTitle, clears directPhone, touches nothing else
curl -X PATCH "https://api.visena.example/acme/api/v1/en/person/xK9mQ2" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/merge-patch+json" \
  -d '{
    "jobTitle": "Head of Operations",
    "directPhone": null
  }'

The word to watch is null. Under PUT an explicit null and an omitted field mean the same thing — both clear the value — because a replace has no way of saying «leave this one alone». Under PATCH they are opposites: null clears, omission preserves. That is why a client that builds one request body and reuses it for both verbs is the classic way to wipe a field nobody meant to touch. If your system holds only part of the record, use PATCH.

The PATCH media type is not application/json. It has to be application/merge-patch+json; plain JSON is refused with 415 before the body is read at all. An empty body {} is legal and a pure no-op — the record comes back unchanged and nothing is written.

Delete

DELETE /{instanceName}/api/v1/{locale}/person/{personId}

DELETE /acme/api/v1/en/person/xK9mQ2
curl -X DELETE "https://api.visena.example/acme/api/v1/en/person/xK9mQ2" \
  -H "Authorization: Bearer $TOKEN"

# 204 No Content

Not every delete destroys, and the defaults are deliberately the safe ones. A company's delete deactivates unless you say otherwise: ?mode=deactivate is the default and reversible, ?mode=delete is the hard delete. An activity's default is ?mode=close. A project's delete is always a soft close. A mode the endpoint does not recognise is a 400 rather than a guess.

Nothing cascades silently. Deleting a document folder that still holds documents, or a document with placements under other records, is refused with 409, and detail counts exactly what would go with it. Resend with acknowledgeCascade=true to confirm.

Response conventions worth knowing

  • An empty field is null, not missing. A full-entity body carries every field of the resource, and the ones with no value are JSON null. A key's presence therefore tells you nothing — read the value. List envelopes are the deliberate exception: a paging link or cursor that does not apply is left out, and its absence is the signal that there is no further page.
  • created / modified and createdBy / modifiedBy carry the persisted audit attribution: RFC 3339 timestamps normalised to UTC, and masked person ids. Supply them on a write and they become the attribution; leave them out and the server stamps «now» and the credential's owner.
  • actedBy is the masked id of the user a mutation acted as. It is filled in only for calls made with an on-behalf credential; a plain credential leaves it null.
  • References are resolved, not trusted. A companyId, groupId, responsiblePersonId or modifiedBy that does not resolve inside the instance fails the whole request with 422. A rejected write leaves nothing half-written.
  • Masked ids are strings. Compare them for equality and nothing else: no ordering, no arithmetic, no constructing one from a number you have somewhere.