Identity & access — reference
The exhaustive surface for identity, app contexts, the scope model, access profiles, roles, and credentials: every method, its parameters, validation rules, limits, the response envelope, error codes, and an honest Notes & limits section. For concepts see explanation.md; for runnable guides see how-to.md.
For the canonical, always-current request and response field shapes, use the generated API reference (rendered from the OpenAPI specification). This page is the durable map and the honest edges — not a regenerated copy of the raw endpoint schemas.
Client construction
import { VectrosClient } from '@vectros-ai/sdk';
const client = new VectrosClient({ token, environment });
| Field | Meaning |
|---|---|
token | The bearer credential: sk_*, ssk_*, or st_*. |
environment | The API base URL, e.g. https://api.vectros.ai (production) or https://api.staging.vectros.ai (staging). |
Identity and access methods live under two sub-clients: client.identity.* (users, orgs,
clients) and client.auth.* (contexts, roles, access profiles, scoped keys, token minting,
the identity-binding check, and the cross-context principal lookup).
Response envelope
List, lookup, and version-history methods return the standard page envelope:
{ data: T[], nextCursor: string | null }
Drain by feeding nextCursor back as the next call's startFrom until it is null. The
identity-binding check (ping) and token minting are not enveloped — they return a plain
object.
Identity plane — client.identity.*
Three dimensions — users, orgs, clients — each with the same lifecycle. All are
tenant-wide and idempotent on the caller-supplied externalId.
| Method | Purpose |
|---|---|
createUser / createOrg / createClient | Create (idempotent by externalId). |
getUser / getOrg / getClient | Fetch one by Vectros id. |
updateUser / updateOrg / updateClient | Full-replace update (PUT) of the body. |
deleteUser / deleteOrg / deleteClient | Delete by id. |
listUsers / listOrgs / listClients | List, filterable; enveloped. |
getUserVersions / getOrgVersions / getClientVersions | Version history; enveloped. |
lookup… | Exact lookup by external id / declared fields (see the generated reference for per-dimension shapes). |
Fields by dimension
| Field | User | Org | Client | Notes |
|---|---|---|---|---|
externalId | ✓ | ✓ | ✓ | Your id; create is idempotent on it. Capped at 256 chars; permissive about characters. |
id | ✓ | ✓ | ✓ | Vectros-assigned UUID; returned on create/get/list. |
email | ✓ | — | — | Users carry email, not name. |
name | — | ✓ | ✓ | Orgs/clients carry name, not email. |
type | ✓ | — | — | HUMAN (default) or SERVICE. |
orgId | — | — | ✓ | A client's owning org. |
payload | ✓ | ✓ | ✓ | Free-form Record<string, unknown> attribute bag, round-tripped as-is. |
status | ✓ | ✓ | ✓ | ACTIVE on create; lifecycle states apply. |
Filters. listUsers / listOrgs accept externalId. listClients accepts externalId
and orgId. All list methods accept limit and startFrom (cursor).
Idempotency semantics. A second create with an existing externalId returns the
existing identity unchanged — the duplicate call's other fields are dropped, it does
not update. Use the update method to change an identity.
CLI equivalents
vectros identity create --type <user|org|client> --external-id <id>
[--name <n>] [--email <e>] [--service] [--org <orgId>] [--metadata <json>]
vectros identity list --type <user|org|client> [--external-id <id>] [--limit <n>]
vectros identity get --type <user|org|client> --id <vectrosId>
vectros identity delete --type <user|org|client> --id <vectrosId>
--name and --email are mutually dimension-specific (--email only for users; --name
only for orgs/clients). --service only applies to users; --org only to clients.
App contexts — client.auth.*
The isolation partition. contextId must match ^[a-z][a-z0-9-]{2,30}$ (starts with a
lowercase letter; lowercase letters, digits, hyphens; 3–31 chars total).
| Method | Purpose |
|---|---|
createAppContext | Create (idempotent by contextId). name is required. Root sk_* only. |
getAppContext | Fetch one by contextId. |
updateAppContext | Update name / description (the path supplies contextId; the body's contextId is required by the schema but ignored — it is immutable). |
listAppContexts | List; enveloped. |
deleteAppContext | Confirm-gated async cascade — see below. Root sk_* only. |
Root-only lifecycle. Creating and deleting an app context require a root sk_* key.
A scoped key or token (ssk_* / st_*) cannot create or tear down a context — not even one
carrying the wildcard * scope. (Get / update / list are reachable with appropriate scope.)
Delete contract. deleteAppContext({ contextId, confirm }) requires confirm to equal
contextId:
- Without a matching
confirm→ 400, and nothing is touched (the rejected delete is a no-op; child roles/profiles still exist). - With a matching
confirm→ accepted (202); the context flipsactive → purgingimmediately and drains all of its records, documents, folders, schemas, roles, and profiles in the background, reachingdeletedwhen the drain completes.
Reserved context. Certain context ids are reserved by the platform and cannot be created
by a tenant — vectros-admin (backs the hosted admin surfaces) and default (the base
context auto-provisioned for every tenant).
Errors. Malformed contextId → 400 with a partner-friendly message. Get on a
well-formed but never-created contextId → 404 (not 500). Cross-tenant probes collapse to
404.
CLI equivalents
vectros context create <contextId> [--name <n>]
vectros context list
vectros context get <contextId>
(The CLI exposes create/list/get; context teardown is performed through the API delete with its confirm gate.)
The scope model
Scope grammar
An allowed action is resource:ops[:qualifier]:
resource— one of the data-plane resourcesrecords,schemas,search,documents,folders,inference(control-plane resources exist for the control surface but are not mintable through the bootstrap gate — see below).ops— any combination of the lettersccreate,rread,uupdate,ddelete. E.g.records:r,records:cru,documents:crud.qualifier— optional tail that narrows (e.g.records:r:intake_form= read only theintake_formrecord type). It never widens.
What does NOT grant access (author the letter form instead):
| Form | Effect at runtime |
|---|---|
records:r, search:r, … | ✓ Grants the named operations. |
* (the single literal wildcard) | ✓ Grants everything — the shape root keys carry. Reserve it. |
A coarse verb (read, write, delete) | ✗ Grants nothing. |
resource:* (operations-wildcard) | ✗ Grants nothing at runtime. Author explicit c/r/u/d letters. |
The operations-wildcard
resource:*is accepted by the bootstrap scope gate (it stays within the data plane), but it does not grant access at the runtime enforcement layer. Always author the explicit letter form. The only wildcard that grants anything is the bare literal*.
dataScope
A map from an ownership field to the list of allowed values:
{ "dataScope": { "clientId": ["client_abc", null] } }
| Field | Meaning |
|---|---|
userId | Confine to rows owned by the listed users. |
orgId | Confine to rows owned by the listed orgs. |
clientId | Confine to rows owned by the listed clients. |
- Enforced as a server-side filter below any caller-supplied filter; cannot be widened by the caller.
- Multiple values in one field's list → union (OR). Multiple fields → intersection (AND).
- Strict by default. A scoped credential must include the matching filter on every list and search call, or the request is rejected (e.g. "clientId is required by token scope").
nullsentinel. Include JSONnullin a field's value list to additively grant access to tenant-level rows (no value for that field). Opt-in only — never implicit.
The bootstrap scope gate (data-plane allowlist)
The CLI / blueprint bootstrap flow mints scoped keys only for the data plane. The allowlist is exactly:
records, schemas, search, documents, folders, inference
Any other resource — the control plane keys, profiles, app-contexts, users,
billing, admin, clients, orgs, or any unrecognized resource — and the literal *
are hard-rejected: the bootstrap mints nothing and exits non-zero. There is no override
flag; control-plane scoped keys are created deliberately in the developer portal.
Access profiles — client.auth.*
The per-principal, per-context permission binding.
| Method | Purpose |
|---|---|
createAccessProfile | Create/bind (idempotent by (context, principalId)). |
getAccessProfile | Fetch one by (contextId, principalId). |
updateAccessProfile | Update scopes/role/status/overrides. |
deleteAccessProfile | Remove the binding. |
listAccessProfiles | List a context's profiles; enveloped. |
listProfilesForPrincipal | Cross-context: every context a principal is bound to; enveloped. |
Profile fields
| Field | Meaning |
|---|---|
principalId | The bound principal: usr_<userId> or key_<keyId>. Must start with usr_ or key_; structural characters like : are rejected with 400. |
scopes | Inline clauses, each { allowed_actions: string[] } (snake_case on the wire). XOR with roleId. |
roleId | Reference to a reusable role. XOR with scopes. |
status | active or suspended. Suspending denies access without deletion. |
identityOverrides | Ownership values stamped onto what the principal touches. Accepts orgId and clientId only (each { value: "<id>" }). userId and the tenant id are sacred and rejected with 400. |
XOR enforcement. A profile carries exactly one of inline scopes or a roleId. Updating
to set one clears the other (empty-string / empty-array sentinels). On create, the unset
half is absent or an empty sentinel.
Idempotency. A repeat create for an existing (context, principalId) returns the
existing profile unchanged.
Cross-context lookup. listProfilesForPrincipal({ principalId }) returns every profile
for that principal across all contexts. A principal with no profiles returns an empty array
(200), not 404. A malformed principalId (e.g. containing :) → 400.
CLI equivalents (vectros access)
vectros access grant --principal <usr_|key_> --context <c> (--role <r> | --actions <csv>)
vectros access revoke --principal <usr_|key_> --context <c>
vectros access list (--context <c> | --principal <usr_|key_>)
vectros access get --principal <usr_|key_> --context <c>
--role and --actions are mutually exclusive (exactly one). --actions mints a
single-clause inline profile. access list requires exactly one of --context (a context's
members) or --principal (a principal's contexts).
Roles — client.auth.*
Reusable, context-scoped, identity-agnostic permission shapes.
| Method | Purpose |
|---|---|
createRole | Create (idempotent by roleId). |
getRole | Fetch by (contextId, roleId). |
updateRole | Update name / scopes. |
deleteRole | Delete — blocked with 409 while a profile still references the role. |
listRoles | List a context's roles; enveloped. |
Role fields
| Field | Meaning |
|---|---|
roleId | The stable role handle within the context. |
name | Human-readable name. |
description | Optional. |
scopes | One or more clauses, each { allowed_actions: string[] }. Roles may be multi-clause — any clause that matches grants access. |
Roles support an ownership placeholder ${{ self.* }} (resolves to the acting principal at
runtime) and a null data-scope sentinel (additively grants tenant-level/owner-less records).
These richer forms are authored through blueprints.
Referential integrity. Deleting a role referenced by a profile is rejected with 409 — remove or re-point the profile first. Deleting an unreferenced role succeeds; the platform does not cascade.
CLI equivalents (vectros role)
vectros role create --context <c> --role-id <id> --name <n> --actions <csv> [--description <d>]
vectros role list --context <c>
vectros role get --context <c> --role-id <id>
vectros role delete --context <c> --role-id <id>
The CLI role create authors single-clause roles from --actions.
Credentials
Types
| Prefix | Lifetime | Scope | Use |
|---|---|---|---|
sk_live_* / sk_test_* | Permanent (revoke to retire) | Wildcard within its tenant | Server-to-server from your own backend. |
ssk_live_* / ssk_test_* | Permanent (revoke to retire) | Bound to a profile; identity-bearing | Agents, bots, long-running workers; audit attribution. |
st_* | 1h default / 24h max | Embedded in the token | Front-end-safe per-session credentials. |
The raw secret of an sk_*/ssk_* is shown once at creation and never re-readable; the
platform stores only a hash.
Scoped key lifecycle — client.auth.*
| Method | Purpose |
|---|---|
createScopedKey | Mint an ssk_* for a principal that already has a profile in the context; raw secret returned once. |
getScopedKey | Metadata for one key (no secret). |
revokeScopedKey | Soft-delete; stops working within ~5 minutes (authorizer cache). |
listScopedKeys | The tenant's keys; enveloped (single page — no cursor input on this endpoint). |
createScopedKey fields: keyName, tenantId, contextId, userId (the bare principal
user id), optional label. A re-issue of an existing (tenant, context, principal, keyName)
tuple returns the key without the secret (the platform never re-discloses) — rotate to
get a fresh secret.
CLI equivalents (vectros key)
vectros key issue --principal <p> --context <c> [--name <n>] [--label <l>] [--format human|raw|env|json]
vectros key list [--principal <p>] [--context <c>]
vectros key get <keyId>
vectros key revoke <keyId>
vectros key rotate --principal <p> --context <c> [--name <n>] [--format …]
key rotate has no dedicated endpoint — it revokes the matching active key and mints a fresh
one. There is no in-place rotation.
Token minting — client.auth.mintToken
const { token, expiresAt } = await client.auth.mintToken({
scope: { allowedActions: string[], dataScope?: {...}, identity?: {...} },
userId?: string, // mint on behalf of a real user in the tenant
expiresInSeconds?: number, // default 3600, max 86400
});
| Field | Meaning |
|---|---|
scope.allowedActions | Required; array of resource:ops[:qualifier] strings. Malformed entries → 400 at mint. |
scope.dataScope | Optional; the data the token may read/write. |
scope.identity | Optional; ownership values stamped onto resources the token creates. |
userId | Optional; must reference a real user in the caller's tenant (unknown id → 400 naming the field). |
expiresInSeconds | Optional; default 3600 (1h), capped at 86400 (24h). |
Returns { token: "st_…", expiresAt: <unix-seconds> }. Tokens cannot be revoked in flight —
expiry is the only lever; mint short.
Identity-binding check — client.auth.ping
Returns the authenticated principal's identity: status, tenantId, environment,
principalType (root_key | scoped_key | token), and principalKeyId. For a
scoped_key, allowedActions is present (and a dataScope if bound to a user/org). For a
token, tokenExpiresAt is present. An invalid credential is denied at the edge with 403.
Error codes
| Code | When |
|---|---|
| 400 | Malformed contextId / principalId / scope token; userId/ownership id not a real row in the tenant; identityOverrides.userId (sacred field); context delete without a matching confirm. |
| 403 | Invalid or unauthorized credential; a scoped action the credential's scope does not permit. Messages are uniform on purpose — they do not reveal which check failed. |
| 404 | Get on a non-existent (or cross-tenant) context/identity; list/create under a non-existent parent context. Cross-tenant probes collapse to 404. |
| 409 | Delete a role still referenced by a profile. |
A scoped credential lacking permission for a list/search filter required by its dataScope
is rejected (strict scope) with a message naming the required field.
Notes & limits
What this surface does not do, stated plainly:
- No identity PATCH. Users, orgs, and clients update via full-replace PUT only — there is no partial-update on the identity plane.
- No schema PATCH on the identity plane and no reparenting of identities — orgs/clients are referenced by id, not moved through a hierarchy.
- Single scope clause on
mintToken. The token-mint endpoint serializes one(allowedActions, dataScope)clause per request. Multi-clause shapes are expressed through a role (referenced by a profile) or a blueprint, not minted directly as a single multi-clause token. - Single-clause access-profile create. The profile-create API accepts one inline clause; reach for a multi-clause role for compound shapes.
identityOverridesisorgId/clientIdonly.userIdand the tenant id are sacred and rejected — a profile cannot forge another user's identity.resource:*grants nothing at runtime. Author explicitc/r/u/dletters. Only the bare literal*grants everything (and that is the root-key shape).sis an advanced op letter, beyondc/r/u/d. A fifth op,s, grants reveal of a type's sensitive fields (e.g.customer:rs= read + reveal-sensitive). It is a per-type capability, not part of the standard CRUD set — the blueprint/bootstrap scope gate accepts onlyc/r/u/d, so packs are authored with those;sis minted deliberately where sensitive-field reveal is intended.- No in-place key rotation. Rotation is revoke-then-reissue; a key's raw secret is shown once and never re-readable.
- Revocation is not instant. A revoked
sk_*/ssk_*keeps working until the edge authorizer cache expires — up to about five minutes.st_*tokens cannot be revoked at all; they expire on their lifetime. Plan offboarding with this window in mind. listScopedKeysis a single page — the endpoint takes no cursor input; filter client-side by context/principal.- Cross-tenant existence is unobservable. Probing for another tenant's id returns the same uniform 404 as a non-existent id; scope-mismatch failures return the same generic shape.
Where to go next
- explanation.md — the concepts: contexts as the isolation moat, the identity plane, the scope model, and the three credential types.
- how-to.md — runnable guides for every method above.
- The generated API reference (rendered from the OpenAPI specification) — canonical, always-current request/response field shapes.
- The blueprint walkthroughs — end-to-end builds that wire contexts, profiles, roles, and scoped keys together.