SAGARIS API keys, the twelve scopes, and rejections

Two things authenticate against SAGARIS: a person, with a session, and a program, with a workspace API key. This page is about the second, plus the part of the first that decides who is allowed to create one.

Who can mint a key

Creating an API key in settings is an admin action. A key made there can carry any scope, long-lived write scopes included, so a lower-privilege member, or a compromised one, must not be able to make one; the gate runs before the request body is even parsed. Four workspace roles pass it: super_admin, org_admin, owner and admin. The same rule applies to registering a webhook subscription, and to revoking a key, for the same reason.

A key needs a name and at least one valid scope. An unrecognized scope string is dropped rather than stored, so a typo cannot smuggle in a permission, and a request whose scopes all get dropped is refused rather than creating a key that grants nothing.

One other path mints a key: an MCP client's OAuth sign-in. The person who allows it needs permission to approve outbound sends rather than an admin role, and the key carries only linkedin.read and linkedin.execute. It is listed under Settings > API keys and revoked like any other key.

The key itself

A key is a single token beginning ros_live_, followed by 256 bits of random data in URL-safe base64. The prefix is deliberate: a leaked key is greppable and recognizable as ours.

The plaintext is returned exactly once, at creation, and is never recoverable. What is stored is the SHA-256 hash of the key plus a short non-secret display prefix, which is what you see in the settings list. Nothing logs the plaintext, and no endpoint echoes it back. If you lose it, revoke the key and mint another.

Scopes

Twelve scopes exist and each operation asserts exactly one. Deny by default: a key that holds no scopes satisfies nothing, and no scope implies another, so a key that can read the Social checklist cannot submit to it until that scope is granted too.

contacts.read
List, retrieve and query contacts (GET /contacts, GET /contacts/{id}, POST /contacts/query) and, through the MCP endpoint, read the Brain views over a contact, account or opportunity.
contacts.write
Create a contact, update a contact's fields, add a tag.
sequences.read
List sequences.
sequences.write
Enroll a contact into a sequence.
deals.write
Create a deal.
activities.write
Log an activity on a contact.
suppression.write
Add an address to the suppression list.
social_checklist.read
Read the Social checklist, what is due now, and one item's context.
social_checklist.submit
Hand an item's output to a person: a draft post in the Social review queue, or a pending approval card. Never publishes, sends or approves.
social_checklist.record
Record an assistant's evidence on an observation item, never over a person's mark.
linkedin.read
Read the LinkedIn actions assigned to the key's connected agent, with recipient, copy and campaign context. Never a send authorization.
linkedin.execute
Read a connected agent's setup and operating brief, report its session, sender health, accepted connections, schedules and runs, sync observed replies and accepted invitations, acknowledge a reconnect, and claim, start and report a LinkedIn action it performs in its own browser session. Claim and start are refused until LinkedIn execution is activated for the environment, and need a person's approval of the action or an account a person set to run automatically.

Presenting a key

Send it as a bearer token. The scheme name is matched case-insensitively; nothing else is accepted.

GET /api/v1/contacts
curl https://www.sagaris.ai/api/v1/contacts \  -H "Authorization: Bearer $SAGARIS_API_KEY"

The workspace is resolved from the key, and the request is pinned to it. There is no workspace parameter on any endpoint, and no request-supplied workspace id is read, so a key can only ever reach the tenancy it was created in.

Why a request is rejected

Every rejection carries a stable machine-readable code alongside its message, so a client can branch without parsing prose. A 401 also carries a WWW-Authenticate header advertising the scheme.

401 missing_key
No Authorization header, or a token that is not shaped like one of our keys.
401 invalid_key
The token does not match any key.
401 revoked_key
The key was revoked.
401 expired_key
The key carried an expiry and it has passed.
403 insufficient_scope
The key is valid but does not hold the scope this operation asserts. The message names the missing scope.
503 db_unavailable
The service cannot reach the key store. This is not an authentication verdict and should be retried, not treated as a bad key.

Failures are recorded as auth events with the reason and the workspace, and a scope denial is recorded distinctly from a failed authentication. Successful use stamps the key's last-used time, which is what the settings list shows; that stamp is best-effort and never blocks an authorized call.

Revoking

Revoking is an admin action too, on the same four roles that can mint in settings, and it covers keys minted through OAuth sign-in. Revocation is a soft flag rather than a delete, so the key id stays referenceable for idempotency records and audit rows, and the next request presenting that key is refused with revoked_key. The update is constrained to the caller's own workspace, so a key belonging to another workspace is reported as not found rather than revoked. Creating a key in settings and revoking any key are written to the workspace configuration audit; minting a key through OAuth sign-in is not, though that key is listed in settings under its connector name.

What a key can never do

  • Reach another workspace. To integrate two workspaces, mint two keys.
  • Force a gated pipeline promotion. An API key holds no workspace role, so it cannot take the manager override that a person can: asking for it is a 403 rather than a silent refusal.
  • Gain a scope by calling a different surface. The MCP endpoint authenticates the key without asserting any single scope, because the scope depends on which tool is called; every tool dispatch then enforces its own. A key with an empty scope list authenticates there and is offered zero tools.

Behaviour on this page is read from

  • src/lib/public-api-key.ts
  • src/lib/public-api-auth.ts
  • src/app/api/api-keys/route.ts
  • src/app/api/api-keys/[id]/route.ts
  • src/lib/admin-access.ts
  • src/app/api/v1/contacts/route.ts
  • src/app/api/v1/contacts/[id]/route.ts
  • src/app/api/v1/linkedin/actions/route.ts
  • src/lib/linkedin/action-api.ts
  • src/lib/oauth/server.ts
  • src/app/api/oauth/authorize/route.ts

Was this page helpful?

Book a demo