Authentication
Every route needs a credential. There are two kinds, and one header that decides which client company or firm a request acts on.
Two doors
| Credential | Sent as | Who uses it | Organization |
|---|---|---|---|
| Syllevo API key | X-Api-Key: syl_live_... or Authorization: Bearer syl_live_... |
Your server, scripts, integrations | Fixed. The key belongs to one organization |
| WorkOS session token | Authorization: Bearer <jwt> |
The Syllevo web app | Chosen per request with X-Organization-Id |
Both headers can carry a bearer token, and the key prefix is what tells them apart. A token
starting with syl_live_ or syl_test_ is treated as an API key. Anything else is verified as a
WorkOS session token.
If you are building an integration, use an API key. Session tokens are issued by the sign-in flow in the web app and are not something you can mint.
API keys
Keys are created in the Syllevo web app under Settings, then API keys, by a member with the
Admin role or higher. They can also be created through the API itself. See
API keys.
What you need to know:
- The full key is shown once, at creation. Syllevo stores only a hash, so a lost key cannot be recovered. Create a new one and revoke the old one.
syl_live_keys are minted in production.syl_test_keys are minted everywhere else.- A key can carry an expiry date. An expired key stops working at that moment.
- A key carries scopes: a set of permissions, each optionally pinned to one client company. A key can never be given a permission the person creating it does not already have.
- A key belongs to exactly one organization and cannot act outside it.
- Revocation takes effect on the next request.
- When the person who created a key is removed from the organization, Syllevo revokes their keys at the same moment. Create integration keys from an account that will stay with the firm.
An unknown key, a revoked key, and an expired key all answer the same way:
{
"status": 401,
"code": "api_key_invalid",
"messageKey": "web.errors.api_key_invalid",
"detail": null
}
That is deliberate. The response does not say which of the three it was, because saying so would tell anyone holding a guessed key whether it ever existed.
The organization header
X-Organization-Id names the organization a session request acts on.
- With an API key: do not send it. The key already names its organization. Sending the header has no effect on which organization you reach.
- With a session token: send it on every organization-scoped route. A person can belong to several organizations, and the token does not say which one they mean.
If a session request reaches an organization-scoped route without naming an organization, the answer is 403:
{
"status": 403,
"code": "organization_required",
"messageKey": "web.errors.organization_required",
"detail": null
}
If the header names an organization the caller is not a member of, the answer is also 403, with a different message key:
{
"status": 403,
"code": "forbidden",
"messageKey": "web.errors.organization_access_denied",
"detail": null
}
Membership is checked before any handler runs, so a non-member never reaches the data.
Routes that are not organization-scoped
A handful of routes answer without an organization, because they exist to find out which organizations you have:
| Route | Why |
|---|---|
GET /v1/users/me, POST and DELETE /v1/users/me/avatar |
The person, not any organization |
GET and POST /v1/organizations |
Listing and creating organizations |
GET /v1/me/invitations and the accept or decline routes |
Invitations you hold, before you are a member |
POST /v1/onboarding/session |
The first call after sign-in, which bootstraps the rest |
GET /v1/status |
Says what your credential is, whatever it is |
GET /v1/registry/company |
A public company lookup, not organization data |
Everything else is organization-scoped.
A wrong path answers 401, not 404
This one surprises people, so it is worth stating plainly.
Syllevo's authorization is default deny: a request must be authenticated before it reaches the routing table's answer. A path that matches no route still goes through that check, so an unauthenticated request to a misspelled path answers 401, not 404.
GET /v1/invoicez HTTP/1.1
Host: api.syllevo.com
{ "status": 401, "title": "Unauthorized" }
If you are getting 401 and you are sure the credential is good, check the path before you check the credential. Send the same request with a valid credential: if the path is wrong you will then get a 404 and know which problem you have.
Note that this particular 401 comes from the framework, not from a Syllevo handler, so its body is
an RFC 9457 problem document rather than the usual ErrorResponse. See
Conventions.
Roles and permissions
There are two axes. Both are enforced at one place inside the API, the same way for session tokens, API keys, and MCP, so a key can never do something the person holding it could not.
Organization roles
| Role | Value | Can do |
|---|---|---|
Member |
0 | Whatever the per-client grants allow, and nothing at the firm level |
Admin |
1 | Everything a member can, plus team, invitations, API keys, firm settings, and exports |
Owner |
2 | Everything an admin can, plus billing, affiliates, ownership transfer, and two-factor enforcement |
Roles serialize as those numbers, not as names. See Conventions.
There is exactly one owner at a time. Ownership moves with
POST /v1/members/{memberId}/transfer-ownership.
Per-client permissions
Access to one client company is a set of permissions, granted per member or per API key scope.
| Permission | Value | Lets the caller |
|---|---|---|
View |
0 | Read the client, its documents, invoices, suppliers, and status |
ManageDocuments |
1 | Upload, delete, restore documents, and decide review items |
ManageSources |
2 | Connect, retag, sync, and disconnect sources, and merge suppliers |
SendReminders |
3 | Pause, resume, and send reminders, and manage client contacts |
ManageSettings |
4 | Change the client's own details and its reminder overrides |
TransmitInvoices |
5 | Reserved for sending sales invoices on. Grantable to an API key, checked by no route yet |
A member can also hold organization-wide access, which grants every permission on every client in the organization, including ones created later.
An Admin or Owner role does not silently grant per-client permissions. There is no admin
fast path. What an admin gets instead is the firm-level routes: team, invitations, keys, settings,
and exports.
Each endpoint on the resource pages names the role or permission it needs.
Checking what a credential can see
GET /v1/status answers with the actor type, the organization the request resolved to, and the
correlation id. It is the fastest way to tell a scope problem from a credential problem. See
Status and month-end completeness.