Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Authentication

ScaiVault accepts bearer tokens issued by ScaiKey. Every request except /v1/health and /v1/health/ready requires one. There are three token flavors, for three different use cases.

Token types#

Type Use when Shape Lifetime
Personal access token A human scripts against the API skt_... Configurable (default 90d)
Client credentials JWT A service calls the API JWT starting with eyJ 1h, refresh via client-credentials flow
User session JWT A user is signed into a ScaiLabs app JWT starting with eyJ 1h, refresh via OAuth refresh token

All three go in the Authorization: Bearer ... header. ScaiVault validates the signature against ScaiKey's public keys (JWKS cached locally).

Use client-credentials tokens for server-to-server traffic. Personal access tokens are fine for scripting and CI, but they belong to a human — if that human leaves, their tokens should be revoked. Services should authenticate as services.

Getting a client-credentials token#

Create a service account in ScaiKey, assign it the scopes it needs, and get back a client_id and client_secret. Then exchange them for an access token:

bash
1
2
3
4
5
6
curl -X POST https://scaikey.scailabs.ai/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=secrets:read secrets:write"
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import os
import httpx

resp = httpx.post(
    "https://scaikey.scailabs.ai/oauth/token",
    data={
        "grant_type": "client_credentials",
        "client_id": os.environ["CLIENT_ID"],
        "client_secret": os.environ["CLIENT_SECRET"],
        "scope": "secrets:read secrets:write",
    },
)
token = resp.json()["access_token"]
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const resp = await fetch("https://scaikey.scailabs.ai/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_id: process.env.CLIENT_ID!,
    client_secret: process.env.CLIENT_SECRET!,
    scope: "secrets:read secrets:write",
  }),
});
const { access_token } = await resp.json();

Tokens last one hour. Your client should cache and re-request on expiry; all official SDKs do this automatically.

Scopes#

A token can only do what its scopes allow. The set of scopes is intersected with the caller's policies — both must permit the action.

Scope What it lets you do
secrets:read Read secret values
secrets:write Create and update secrets
secrets:delete Delete secrets
secrets:list List secrets and metadata
secrets:rotate Trigger rotations
policies:read View policies
policies:write Create, update, delete policies
audit:read Query audit logs
audit:export Export audit data
pki:issue Issue and sign certificates
pki:admin Manage PKI roles, CAs, revocation
dynamic:read View dynamic engines, roles, leases
dynamic:generate Generate dynamic credentials
dynamic:manage Configure engines and roles
dynamic:revoke Revoke leases
subscriptions:read / subscriptions:write Consume events
federation:read / federation:write Manage federated backends
identity:read / identity:admin Read identity cache / trigger sync
admin All of the above

Ask for the narrowest set of scopes that does the job. A token that can only read from one path is dramatically less dangerous than an admin token.

Using the token#

Every request:

bash
1
2
curl -H "Authorization: Bearer $SCAIVAULT_TOKEN" \
     https://scaivault.scailabs.ai/v1/secrets/app/db/password

If you omit the header, you get 401 authentication_required. If the token is malformed or expired, 401 token_expired. If the scope is insufficient, 403 access_denied.

Request headers#

Header Required Description
Authorization Yes Bearer <token>
Content-Type On PUT/POST/PATCH application/json
X-Request-ID No Client-supplied ID for end-to-end tracing
X-Partner-ID No Explicit partner context (partner admins)
X-Tenant-ID No Explicit tenant context (partner admins acting cross-tenant)

Tenant context#

The token's tenant_id claim determines which tenant you act as. Most API paths (/v1/secrets/*, /v1/policies/*) are tenant-scoped — you only see and touch your own tenant's data.

Three exceptions:

  • Partner admins can use the /v1/t/{tenant_id}/ prefix to act as a specific tenant under their partner:

    bash
    1
    GET /v1/t/tnt_acme_dev/secrets/app/db/password
    
  • Partner-scoped secrets live under /v1/partner/secrets/* and are visible to every tenant under the partner. Useful for things like "shared trust anchors" or "partner-wide default configs".

  • System endpoints like /v1/identity/partners have their own scope checks.

See Multi-tenancy for the full model.

Tokens and rotation#

Service-account client secrets should be rotated periodically. The recommended pattern: store the secret in ScaiVault (yes, really), rotate it on a schedule, and let your service fetch it at startup.

Chicken-and-egg: the service needs some credential to talk to ScaiVault. That credential — the bootstrap token — is usually injected by your deployment platform (Kubernetes service account token, AWS IAM role, Nomad Workload Identity, GCP Workload Identity) and exchanged for a ScaiKey JWT on boot.

What's next#

Updated 2026-05-17 13:26:50 View source (.md) rev 2