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.
Version note. The generic identity-entity surface (
/v1/entities/{namespace},/v1/namespaces) shipped in 0.35.0, replacing the dedicated/v1/orgsand/v1/clientsendpoints in a clean break — there is no compatibility shim. A client older than0.35cannot reach this surface; upgrade before authoring against this page.
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,
identity entities, namespaces) 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.*
Two dimensions on this plane: the fixed user surface, and the generic identity
entity surface, addressed by namespace. org and client are two namespaces that
are always registered — they carry no dedicated endpoints or fields of their own; you
create, read, and list entities in them exactly as you would in any namespace you
register yourself.
Users
| Method | Purpose |
|---|---|
createUser | Create (idempotent by externalId). |
getUser | Fetch one by Vectros id. |
updateUser | Full-replace update (PUT) of the body. |
deleteUser | Delete by id. |
listUsers | List, filterable by externalId; enveloped. |
getUserVersions | Version history; enveloped. |
| Field | Type | Notes |
|---|---|---|
externalId | string | Your id; create is idempotent on it. Capped at 256 chars; permissive about characters. |
id | string | Vectros-assigned UUID; returned on create/get/list. |
email | string | Users carry email, not name. |
type | enum | HUMAN (default) or SERVICE. |
payload | object | Free-form Record<string, unknown> attribute bag, round-tripped as-is. |
status | string | ACTIVE on create directly; PENDING | ACTIVE | SUSPENDED over the lifecycle. PENDING is server-managed — you cannot set it directly (below). |
schemaId | string | Optional governing schema. When set, payload is validated + lookup-indexed against it, and a token carrying users:s:<schemaType> (the type the schema declares) can reveal that type's sensitive fields on this user — the same mechanism as entities:s:<namespace>. |
Inviting a sub-user — client.auth.*
An alternate creation path that binds an access profile in the same call, so the invitee
arrives already permissioned. Lives under client.auth.* (not client.identity.*) because
it's driven by the access-profile shape.
| Method | Purpose |
|---|---|
createInvite | Create a PENDING user and bind an access profile to it, by email. Idempotent on (contextId, email) — a repeat invite rotates the token and resends. Requires users:c; if the pair already has an outstanding invitation, also requires users:r and users:u (the resend path), else 409. |
resendInvite | Rotate the token and extend expiry on an outstanding invitation, identified by (contextId, email); re-delivers if sendEmail. Invalidates any previously issued accept link. Does not change the bound permissions. Requires users:c, users:r, and users:u. |
updateUser | Also the accept path: a PUT carrying status: 'ACTIVE', inviteToken, externalSubject, and emailVerifiedAttestation: true moves a PENDING user to ACTIVE. |
CreateInviteRequest fields:
| Field | Type | Notes |
|---|---|---|
email | string | Required. Rejected with 409 if it already belongs to an active/suspended member of contextId, or already has an identity elsewhere in the account — an email belongs to at most one tenant per account (a live and a test environment can't share one). |
contextId | string | Required. Must reference an existing app context in your live tenant — invitations always land in the live tenant, regardless of which environment the calling credential authenticates against; the invitee's access profile is bound to it. |
accessProfile | { roleId } | { scopes } | Required. Exactly one of the two, same XOR as a standalone access profile. A roleId naming no role in the context is rejected with 400. The resolved scope (the role's, or the inline scopes) is checked against the caller's own scope and rejected with 403 if it exceeds it — you cannot invite someone into more than you hold. |
acceptUrl | string | Required when sendEmail is true (the default). Your https landing page; the token is appended as a t query parameter. IP-literal hosts are rejected. |
sendEmail | boolean | Default true. false skips delivery and returns inviteToken + acceptLink in the response instead, for you to deliver through your own provider. |
fromName | string | Optional display name for the invitation email's From header. Default Vectros; max 100 chars, no newlines. |
ttlSeconds | integer | Optional. Default 604800 (7 days); must be between 3600 (1h) and 2592000 (30 days). |
firstName | string | Optional; personalizes the email greeting only — not stored or returned. |
lastName | string | Optional; accepted but currently unused (not stored, returned, or applied to the greeting). |
CreateInviteResponse fields: userId (stable across accept), inviteExpiresAt (ISO-8601;
accept attempts after this are rejected), emailSent (true only when sendEmail was true and
delivery succeeded), and — only when sendEmail was false — inviteToken and acceptLink.
Treat inviteToken as a credential: holding it is sufficient to accept the invitation.
While an invitation is outstanding: email cannot be changed on the PENDING user by a
plain update — delete and re-invite to redirect it. Deleting a PENDING user also removes its
bound access profile. Once accepted (PENDING → ACTIVE), email is editable again and
externalSubject is treated as immutable.
Identity entities — /v1/entities/{namespace}
One CRUD-plus-lookup surface for every entity in every entity-backed namespace, including
org and client. The namespace is a path parameter, not a body field — it is
immutable per entity and set once, at create.
| Method | Purpose |
|---|---|
createEntity({ namespace, body }) | Create (idempotent by (namespace, externalId)). ?upsert=true overwrites an existing match instead of returning it unchanged. |
getEntity({ namespace, id }) | Fetch one by Vectros id. |
updateEntity({ namespace, id, body }) | Full-replace update (PUT). |
deleteEntity({ namespace, id }) | Delete by id. |
listEntities({ namespace, userId?, externalId?, scope?, type?, field?, value? | from?+to? | prefix?, order?, startFrom?, limit? }) | List, and — via the type/field/… params — the schema-field lookup surface (non-sensitive fields); enveloped. |
lookupEntities({ namespace, body }) | Body-based lookup for a sensitive schema field (the value travels off the URL); enveloped. |
getEntityVersions({ namespace, id }) | Version history; enveloped. |
Fields (EntityRequest, on create/update):
| Field | Type | Notes |
|---|---|---|
externalId | string | Required on create. Your id, unique within the namespace; create is idempotent on it. |
name | string | Human-readable name. |
status | enum | ACTIVE (default) or SUSPENDED. Suspended entities are retained but blocked from new operations. |
payload | object | Free-form attribute bag. On update, replaces the stored payload in full (not merged); omit to leave it unchanged. |
schemaId | string | Optional governing schema (validates + lookup-indexes payload). Must belong to your account. |
scopes | string[] | Parent ownership edges, each <namespace>:<value> — at most two, each in a namespace different from the entity's own. On update, an explicit scopes replaces the full parent set; omit it to leave ownership unchanged. The entity's own reference is accepted-and-ignored (so you can round-trip a GET's scopes value unchanged); any other value in the entity's own namespace is rejected — a parent always crosses namespaces. For a scoped credential, the update is authorized against a single scope clause that must cover every namespace the change touches — including a parent you are removing (dropping a label is authorized exactly like adding one). Because PUT is a complete declaration, re-sending an unchanged parent that sits in a namespace your granting clause doesn't cover makes the whole PUT a 403 naming that namespace; with no PATCH on the identity plane, the remedy is to send only the labels your clause covers. |
Fields (EntityResponse, on read):
| Field | Type | Notes |
|---|---|---|
created | boolean | Create-response only: true for a new entity, false for an idempotent-return or upsert-overwrite. |
id | string | Vectros-assigned UUID. |
namespace | string | The entity's namespace (present on responses; not a request field). |
externalId | string | As supplied at create. |
name | string | |
status | string | ACTIVE | SUSPENDED. |
scopes | string[] | The entity's effective scopes: its own reference (<namespace>:<id>) followed by its parent edges, e.g. ["team:<id>", "org:<id>"]. |
payload | object | Sensitive fields masked ([redacted]) unless the token carries entities:s:<namespace>. |
schemaId / schemaVersion | string / number | The governing schema and the version in effect when last written. |
createdAt | string | ISO-8601 UTC. |
Namespaces — /v1/namespaces
Declares whether a scope:<namespace> value is a free-form string (the default) or must
resolve to a real identity entity. org and client are always-present, immutable
built-ins.
| Method | Purpose |
|---|---|
registerNamespace(body) | Register a namespace. Requires specificityRank. Root sk_* only. |
getNamespace({ namespace }) | Fetch one. Open to any credential. |
listNamespaces | List; always includes org/client; enveloped. Open to any credential. |
updateNamespace({ namespace, body }) | Update entityBacked / defaultSchemaId / specificityRank. Root sk_* only. |
deleteNamespace({ namespace }) | Delete. Root sk_* only. |
| Field | Type | Notes |
|---|---|---|
namespace | string | 2–32 chars, lowercase-first grammar (^[a-z][a-z0-9_-]{1,31}$). Immutable once registered. org/client, and the reserved names below, cannot be registered. |
entityBacked | boolean | Default false. true ⇒ every scope:<namespace> value platform-wide must resolve to a real entity in this namespace (fail-closed existence check on create); false ⇒ free-form string, grammar-only — and note "grammar-only" is not "unconstrained": see the value row below. |
(the <value> half) | string | 1–128 chars, ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$ — a letter or digit first, then letters, digits, _ or -. Applies wherever a scope value is written or filtered: the scopes array, ?scope=, an access profile's identityOverrides, and a scoped token's identity. Deliberately excludes : (a value becomes part of a storage key whose parser splits on colons) and $/{/} (a stored value is substituted into a scope clause and re-parsed, so a placeholder-shaped value would be read back as a matcher). Unlike the namespace, a value may be mixed-case and may start with a digit — entity ids are UUIDs. |
defaultSchemaId | string | Optional default governing schema for entities created in this namespace. |
specificityRank | integer | Required on create; account-unique. An integer position in your account's specificity order, used to break a tie when a caller holds two scope dimensions at once during basedOn schema resolution — the higher-ranked namespace's variant wins. Optional on update (omit to leave unchanged). Leave headroom between values (10, 20, 30, …) so a later namespace can slot in between without renumbering. |
reserved | boolean | (response only) true for the org/client built-ins. |
createdAt | string | (response only) Absent for the built-ins (they are platform constants, not stored rows). |
Reserved namespace names — never registerable, because they collide with a fixed
schema-reference surface or an /v1/entities/{namespace}/... sub-path: record,
document, entity, user, self, tenant, context, scope, versions, lookup
(plus org/client, the built-ins).
Referential integrity. A namespace cannot be deleted, or updated from
entityBacked: true to false, while entities still exist in it — both reject with 409.
CLI equivalents
vectros identity create --type <user|namespace> --external-id <id>
[--name <n>] [--email <e>] [--service] [--scope <ns:value>...] [--metadata <json>]
vectros identity list --type <user|namespace> [--external-id <id>] [--limit <n>]
vectros identity get --type <user|namespace> --id <vectrosId>
vectros identity delete --type <user|namespace> --id <vectrosId>
--type takes user (the fixed principal surface) or any namespace — org/client
(built in) or one you've registered. --name and --email are mutually dimension-specific
(--email only for user; --name only for an entity namespace). --service only
applies to user; --scope <namespace:value> (repeatable, ≤2) only applies to an entity
namespace. There is no CLI command for the namespace registry itself — register a
namespace via the SDK or API with a root key (see how-to.md); the CLI's
identity command only creates/reads/lists/deletes entities inside a namespace that
already exists.
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 (API). On the API, 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.) Account owners additionally have a self-service path that does
not involve the root key: the dashboard's Contexts page and vectros context destroy both
perform the same confirm-gated teardown through an owner-gated server-side route, so no
teardown-capable credential is ever held by the browser or stored by the CLI.
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>
vectros context destroy <contextId> [--force] [--tenant test|live]
(destroy is the same confirm-gated async cascade as the API delete: interactively it asks
you to re-type the context id, and --force skips that prompt — required when stdin is not
a terminal. It authenticates as the account owner, so it works without the root key.)
The scope model
Scope grammar
An allowed action is resource:ops[:qualifier]:
resource— one of the data-plane resourcesrecords,schemas,search,documents,folders,inference, or a control-plane resource:entities(below, qualified by namespace) orusers(your tenant's own user identities —users:c,users:r,users:u,users:d; the developer portal's scope editor also offersusers:crudas a one-click shorthand for all four).userstakes no qualifier onc/r/u/d— a qualifier there is rejected — but does on the fifth op letter,s(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;entities:cru:org= create/read/update entities in theorgnamespace only). It never widens.
entities follows the same resource:ops:<qualifier> grammar as records, with the
qualifier naming a namespace rather than a record type — entities:c:team grants
create on team entities only. A fifth op letter, s, reveals sensitive payload fields —
entities:s:<namespace> for entities, and users:s:<schemaType> for users that carry a
governing schema (schemaId) — see data-model
reference for the general s op.
What does NOT grant access (author the letter form instead):
| Form | Effect at runtime |
|---|---|
records:r, search:r, entities:r:org, … | ✓ 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:*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*.
Managing the namespace registry itself (POST/PUT/DELETE /v1/namespaces) is not a
grantable scope at all — it is root-key-only regardless of any scope a token carries;
namespaces:* is not a recognized resource.
dataScope
A map from an ownership dimension to the list of allowed values. userId is its own fixed
dimension; every other dimension is a namespace, keyed scope:<namespace>:
{ "dataScope": { "scope:client": ["client_abc", null] } }
| Key | Meaning |
|---|---|
userId | Confine to rows owned by the listed users. |
scope:<namespace> | Confine to rows whose scopes include one of the listed <namespace>:<value> entries — scope:org, scope:client, or any namespace you registered. |
- Enforced as a server-side filter below any caller-supplied filter; cannot be widened by the caller.
- Multiple values in one dimension's list → union (OR). Multiple dimensions → intersection (AND).
- Strict by default. A scoped credential must include the matching filter on every list
and search call (
?userId=, or?scope=<namespace>:<value>for a namespace dimension), or the request is rejected (e.g. "scope:org is required by token scope"). nullsentinel. Include JSONnullin a dimension's value list to additively grant access to tenant-level rows (no value for that dimension). Opt-in only — never implicit.?scope=takes one entry per query — filtering on two namespace dimensions at once in a single call isn't expressible;?userId=plus one?scope=is. Repeated?scope=may land later.- Reads vs. writes on an unnamed dimension. A dimension
dataScopesays nothing about doesn't narrow reads — but it also grants no way to write into it. To place data in a given dimension, that dimension must be named indataScope, either explicitly or via the"*"default-dimension form (a role clause's{"*": [...]}entry — see Role fields above).
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, entities (creating or managing identity entities, including
org/client), 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. Must name a role that exists in the context — a roleId with no matching role is rejected with 400 naming it, on create, update/upsert, and when a scoped-key mint resolves the bound profile's role. (Previously accepted and stored, leaving a profile that could never authenticate — that gap is closed.) |
status | active or suspended. Suspending denies access without deletion. |
identityOverrides | Ownership values stamped onto what the principal touches — each a bare entity-id string keyed by scope:<namespace> (e.g. {"scope:org":"<entity-id>"}), reserved (scope:org, scope:client) or one you registered, up to two total. The value is authorized like scopes/roleId — and this applies even to an update whose body carries only identityOverrides: a scoped credential may override only to a scope:<namespace> value it itself holds (403 otherwise); a root key's override value must reference an entity that exists (400 naming the value otherwise). userId and the tenant id are sacred and rejected with 400. The same authorization also runs on the value being replaced: changing or clearing an entry — including wiping identityOverrides via an empty map — is 403 unless the caller holds the value it would overwrite, and deleteAccessProfile is refused the same way if the profile's identityOverrides holds a value the caller doesn't hold. Unaffected: giving your own identity to a profile with none, editing or deleting a profile whose identity is already yours, omitting identityOverrides entirely, and root API keys (exempt throughout). |
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). Bind identityOverrides with
--identity-overrides '{"scope:org":"...","scope:group":"..."}'.
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. |
Role clauses accept a placeholder grammar in data_scope, in addition to literal values:
| Placeholder | Resolves to |
|---|---|
${{ self.userId }} | The acting principal's own userId. |
${{ self.scope.<namespace> }} (e.g. ${{ self.scope.org }}) | The acting principal's own value in that namespace. |
${{ any }} | Any value present in that dimension — deliberately not a row with no value there. Combine with null in the same list to also match owner-less rows. |
${{ under.self.userId }} | A value whose immediate parent is the principal's own userId. One level only, not a full ancestor walk. |
${{ under.self.scope.<namespace> }} | A value whose immediate parent is the principal's own value in that namespace — e.g. lets a credential confined to an org act on the clients under it without naming each client at mint time. |
A dimension key of "*" (e.g. {"*": ["${{ any }}", null]}) states a default rule for
every dimension the clause doesn't name explicitly; a dimension named directly in the same
clause always takes precedence over the "*" default. A null data-scope sentinel
additively grants tenant-level (owner-less) records. Any ${{ ... }} spelling other than
the forms above is rejected at authoring time with a 400 — it is never silently stored
as a literal that matches nothing. These placeholder 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. The caller must also hold the identity the bound profile carries — 403 if binding to a profile whose identity the caller doesn't hold (a root key is exempt). |
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. Root sk_* only.
const { token, expiresAt } = await client.auth.mintToken({
scope: { allowedActions: string[], dataScope?: {...}, identity?: {...} },
userId?: string, // mint on behalf of a real user in the tenant
contextId?: string, // target an existing app context (defaults to `default`)
expiresInSeconds?: number, // default 3600, max 86400
});
Minting a scoped token is a root-key operation end to end — a scoped key or token
(ssk_*/st_*) cannot call this endpoint at all, so there is no confined credential for
contextId to let escape its own context.
| Field | Meaning |
|---|---|
scope.allowedActions | Required; array of resource:ops[:qualifier] strings. Malformed entries → 400 at mint. |
scope.dataScope | Optional; { userId: [...] } and/or { "scope:<namespace>": [...] } entries — the data the token may read/write. |
scope.identity | Optional; { userId: "..." } and/or { "scope:<namespace>": "..." } — 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). |
contextId | Optional; the app context the minted token operates in. Omit to inherit default. Must reference an existing context — an unrecognized value returns a uniform 404. Combine with an omitted userId/scope.identity to mint a token that creates ownerless (unattributed) resources in that context — e.g. the shared lineage base for a new record type (see basedOn in the data model reference), which every other owner's schema of that type then points back to. |
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 dataScope.scopes reports its resolved
scope:<namespace> bindings if any. 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 / namespace name; an unrecognized ${{ ... }} placeholder spelling in a role's data_scope; a scope value with punctuation outside letters/digits/_/-, or not starting with a letter or digit; userId/ownership id not a real row in the tenant; a roleId that doesn't reference an existing role, on profile create/update/upsert, an invite's accessProfile, or a scoped-key mint bound to it; a scopes entry naming an entity's own namespace (a parent must cross namespaces); a scope:<namespace> value that doesn't resolve to a real entity in an entity-backed namespace — validated on update as well as create, naming the missing parent; identityOverrides.userId (sacred field); context delete without a matching confirm; more than two scopes/parent dimensions; a namespace register/update omitting specificityRank, or reusing one already taken in the account; a malformed invite email, a non-https or IP-literal acceptUrl, or a ttlSeconds outside 1h–30d; a plain update attempting to change email on a PENDING user. |
| 403 | Invalid or unauthorized credential; a scoped action the credential's scope does not permit; a create or update whose resulting ownership placement no single scope clause of the credential permits — including attributing an item to another user without a clause constraining the userId dimension, and setting a scopes/identityOverrides value the scoped credential does not itself hold; changing, clearing, or deleting an identityOverrides value the caller doesn't itself hold; minting a scoped key bound to a profile whose identity the caller doesn't hold; an invite's resolved accessProfile (its roleId's scopes, or its inline scopes) exceeding the caller's own scope; registering, updating, or deleting a namespace without a root key. Messages are uniform on purpose — they do not reveal which check failed. |
| 404 | Get on a non-existent (or cross-tenant) context/identity/namespace; list/create under a non-existent parent context; resendInvite on an (contextId, email) pair with no outstanding invitation. Cross-tenant probes collapse to 404. |
| 409 | Delete a role still referenced by a profile; delete a namespace, or flip it from entityBacked: true to false, while entities still exist in it; createInvite on an email already active/suspended in the context, already invited without the caller holding users:r+users:u to resend it, or already an identity elsewhere in the account (the last case alone returns a structured email_already_associated body — the first two are deliberately indistinguishable). resendInvite has no 409 of its own — a non-matching (contextId, email) is a 404, not a collision. |
A scoped credential lacking permission for a list/search filter required by its dataScope
is rejected (strict scope) with a message naming the required dimension.
Notes & limits
What this surface does not do, stated plainly:
- No identity PATCH. Users and identity entities update via full-replace PUT only — there is no partial-update on the identity plane.
- No reparenting beyond
scopes. An entity's parents are set through itsscopesarray (full replace on update); there is no separate move/reparent operation, and a parent edge always names another entity's<namespace>:<id>— orgs and clients (and any namespace) are referenced by id, never nested through a path. - At most two
scopesentries per entity, and per record/document/folder. Same cap as the record ownership model always had; a value in an entity's own namespace is rejected as a "parent." ?scope=is single-valued. One<namespace>:<value>filter per list/search call; pair it with?userId=for a second dimension, but two namespace dimensions can't be filtered simultaneously in one call.- 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.
identityOverridesisscope:<namespace>only, capped at two.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.entities:s:orgreveals sensitiveorgpayload fields;customer:rssimilarly for a record type). It is a per-resource 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.- The namespace registry is never scope-mintable. Registering, updating, or deleting a
namespace always requires a root key — there is no
namespaces:c(or similar) scope to grant a lesser credential that ability. - 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.
- This was a clean break, not an additive migration (0.35.0). The old
/v1/orgs,/v1/clients,orgId/clientIdfields,orgs:<verb>/clients:<verb>scopes, and the${{ self.orgId }}/${{ self.clientId }}placeholders are gone outright — there is no compatibility shim and no deprecation window. Every removed form fails loudly (a compile error against a generated SDK, or a400naming its replacement), never silently.
Where to go next
- explanation.md — the concepts: contexts as the isolation moat, the identity plane, namespaces, 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.