Guide
Concepts
Seven ideas explain every request you will ever send. This page says what each one is and how it relates to the others; the pages after it use them without stopping to explain.
Authorization: Bearer. The scopes are asked for when the token is minted, out of the set the credential holds, and are then checked on every single request.Instances
Every Visena customer runs in an instance of its own, and the instance name is the first segment of every URL you call. Your base URL comes from onboarding; the examples throughout this documentation use https://api.visena.example and the instance name acme.
The shape is /{instanceName}/api/v1/{locale}/person: instance name, version, locale, resource. Resource paths are singular — /person, not /persons — and there is no partner/ segment anywhere in the API, on any plane. Nine resources sit behind that prefix: person, company, company-template, project, project-template, activity, document, document/folder and document/changes.
The instance is the isolation boundary, not just a routing prefix. A credential belongs to exactly one instance and the tokens it produces work only there: point a token at another instance's URL and you get 403, not a partial answer. A first segment that names no instance at all answers 404.
So if you integrate with several Visena customers, you hold one credential per customer, mint one token per customer, and keep their data apart on your side too. There is no cross-instance query and no call that lists the instances you have access to — the instance name is something you are told, not something you discover.
Credentials and tokens live under the same instance prefix but on a different plane: /{instanceName}/auth/api/v1/… rather than /{instanceName}/api/v1/….
Credentials
A credential is a long-lived clientId + clientSecret pair. It is created inside the customer's instance and authenticated with a Visena user session — the X-ACCESS-TOKEN cookie of a logged-in user, not a bearer token. That is deliberate: a machine credential cannot create more credentials. In most integrations it means the customer's Visena administrator creates the credential and hands you the pair through a secure channel.
The pair is owned by a real user in that instance, and everything your integration does is attributed to that owner in Visena's history and audit records. That single fact is the reason for the housekeeping rules: one credential per integration, one per environment, labelled after the system that holds it. When someone inside the customer's organisation later asks who wrote a record, the answer is only as precise as your labelling was.
The clientSecret is shown exactly once, in the response that creates it. Visena stores a one-way hash and can never show it again; there is no reveal and no reset, only create-and-revoke.
Server-to-server only. Credentials and tokens belong on your backend. Never embed either in a mobile app, a browser page or any code you ship to end users — anyone holding the secret can act as your integration, with that owner's name on the result.
Creating, listing, revoking and rotating a credential — and exchanging one for a token — is Credentials and tokens.
Access tokens
You never send the credential to the business API. You exchange it — one standard OAuth 2.0 client_credentials request against /{instanceName}/auth/api/v1/token — for a short-lived access token, and send that on every call as Authorization: Bearer ….
Three properties decide how you write your client:
- The token is opaque. It is an unreadable string, not a JWT. Do not decode it, and do not look inside it for the expiry, the scopes or the user — the token response tells you those next to it.
expires_incomes from the response, every time. It is the number of seconds until the token expires. Read it and act on it; never hardcode a lifetime, because the number is the platform's to change.- There is no refresh token — by design. When a token expires you repeat exactly the same request. Mint once, cache, reuse until shortly before expiry, then mint again; do not mint one per API call.
A token stops working for four reasons, and they are worth telling apart: it expired (401), the credential behind it was revoked (401 — tokens already minted stop working within about a minute), it was pointed at another instance (403), or it was asked for something outside its scopes (403). Errors and troubleshooting has the full table.
Scopes
A credential holds a set of scopes. A token carries the scopes you asked for when you minted it — the credential's full set, or any subset of it — and every request is checked against the token's set.
| Scope | Allows | Family |
|---|---|---|
| read | All reads — GET requests, lists, downloads, change feeds. | Plain |
| write | All mutations — POST, PUT, PATCH, DELETE. | Plain |
| act-as-user:read | The same reads, from an on-behalf credential. | On-behalf |
| act-as-user:write | The same mutations, from an on-behalf credential. | On-behalf |
Scope is mandatory and explicit. A token request with no scope parameter is rejected with 400 invalid_scope; so is a request for a scope this credential does not hold. The platform never guesses what you meant to ask for, and there is no default.
Enforcement then happens per request: a GET needs the read scope, and every POST, PUT, PATCH and DELETE needs the write scope. A token minted with read alone answers 403 on every mutation — which is exactly what reads work, writes fail means when you see it.
Ask for what the job needs rather than for everything the credential could give. A nightly exporter holding only read cannot damage the customer's data even if it is compromised, and that reduction is worth having in writing: see Read-only access.
Plain vs. on-behalf credentials
Credentials come in two families, and a credential belongs to exactly one of them for its whole life. The difference is which user the platform records as having done the work.
A plain credential acts as its owner — the user it was created under. Everything it writes carries that user as the responsible one, and its scope family is read and write. An on-behalf credential acts for one specific named user in the instance. It is issued by a customer administrator through Visena's administration tooling rather than the self-service credential API, it is bound to that one user, and its scope family is act-as-user:read and act-as-user:write.
The family says who the token acts as. How wide within that family the credential reaches is a separate choice, made when it was created: read and write registers both scopes in the family, read only just the read one. A credential created as read-only can never mint a write token — see Two ceilings, not one.
| Property | Plain | On-behalf |
|---|---|---|
| Scope pair | read · write | act-as-user:read · act-as-user:write |
| Issued through | The credential API, inside the instance, by a logged-in user. | Visena's administration tooling, by a customer administrator. |
| Acts as | The user who owns the credential. | The one named user it was issued for. |
| Recorded on a write | The owner. | The user it acted for and the actor behind it. |
| Mutation response | — | Carries actedBy, the masked id of the acting user. |
The consequence is about identity, not about capability: apart from the scope pair and the recorded identity, the two families behave identically — the same URLs, the same request bodies, the same status codes. What changes is the name a caseworker sees on the record afterwards, and what an audit answers when asked who did this. Pick on-behalf when the work must read as a particular person's — a caseworker filing a document, an adviser registering an activity. Pick plain when your integration is the author in its own right: a nightly import, an export, a dashboard.
You cannot mix the families. Each credential holds exactly one pair, so asking an on-behalf credential for plain read write — or a plain credential for act-as-user:* — fails with 400 invalid_scope at the token endpoint, before any data is touched.
The family is a design decision, taken before you write a line of code. It is fixed for the life of the credential: there is no per-request switch, and a plain credential cannot be promoted. And because an on-behalf credential is bound to one user, acting for several users means one credential per user — each issued by the customer's administrator, each held separately by you. An integration that will one day file work in named users' names is much cheaper to build that way from the start than to convert later.
Masked ids
Every entity id crossing the API is an opaque masked id — a string like xK9mQ2, never a number. It is not the record's database key: the platform's internal keys stay inside the platform, and the API hands out a masked form instead. That is why you cannot guess an id, cannot count them, and cannot infer from one how many records a customer has.
On your side, that makes the masked id an opaque foreign key, and the storage rules follow from the word opaque:
- Store it as text, exactly as received. Full length, unaltered. It is not numeric, so an integer column cannot hold it; the ids in this documentation are mixed case, so store and compare them case-sensitively and keep any ORM or database collation from folding the case away.
- Do not assume a length or an alphabet. The examples here are six characters, and nothing promises that they stay six. Give the column room instead of pinning it to what today's ids look like.
- Echo it back verbatim. It is the only handle the API accepts for that record, and it is the join key between your rows and Visena's.
- Derive nothing from it. Two ids cannot be compared for order, so an id is neither a sort key nor a high-water mark for a sync — use the time-windowed lists in Lists, paging and sync for that.
- Never construct one. An id you did not receive resolves to nothing: as a reference in a write body it fails with 422 and nothing is written; as a path segment it is 404.
Masked ids turn up in a few places where a first-time integrator does not expect them. The id that comes back when you create a credential is a masked id, and revocation uses that id rather than the clientId. The Location header on a successful create carries the new record's path, ending in its masked id, and the same id is in the response body. actedBy on a mutation response is a masked id too, and so is every reference field that points at another record.
An id is only meaningful inside its instance. An id from one customer's instance resolves to nothing in another, and the same physical person at two Visena customers is two unrelated records with two unrelated ids. If your system spans several customers, key your side on the instance name plus the id, not on the id alone.
Locale
The {locale} segment sits between the version and the resource. The documented values are en and no — note that the API's Norwegian is no, not the nb this documentation site uses in its own URLs.
What the segment decides is the language of the human-readable text in responses. The clearest case is an error: the detail member of an RFC 9457 problem document is the string you would put in front of a person, and it is localized by this segment. What the segment does not decide is the shape of the payload — field names, enum values and ids are the API contract and read the same in every locale, which is why the endpoint reference on this site is in English in both language trees.
The locale governs what the API returns, not the product. origo's own interface is Norwegian either way: putting en in your URLs does not translate a single screen, button or report for the customer's staff. So if your integration shows Visena data beside a Norwegian screen, no is what keeps your strings consistent with the words next to them.
Pick one locale and use it consistently across the integration, including on the credential endpoints, which carry the segment too. Consistency is a practical matter as much as an aesthetic one: if half your calls say en and half say no, the error text sitting in your log is not the error text you can search your code for.