Authentication
Each package authenticates a little differently because each runs in a different trust context. This page covers the client and server SDKs; the MCP SDK uses an Auth0 PKCE flow documented separately in MCP Authentication.
Server SDK — API key
Section titled “Server SDK — API key”The server SDK runs on your trusted backend and authenticates with a secret API
key (or an OAuth access token). The apiKey is required unless you pass
accessToken.
import { DivinciServer } from "@divinci-ai/server";
const divinci = new DivinciServer({ apiKey: process.env.DIVINCI_API_KEY, // never hardcode});
// Verify the key is valid and activeconst ok = await divinci.validateApiKey();Client SDK — three ways to identify the caller
Section titled “Client SDK — three ways to identify the caller”The client SDK always needs a releaseId. Beyond that, choose one identity
mechanism depending on how much you trust the runtime.
1. User token (recommended for browsers)
Section titled “1. User token (recommended for browsers)”Pass a short-lived JWT your backend issues for the signed-in user. Internally the
client wraps it in a SimpleTokenStorage.
import { DivinciClient } from "@divinci-ai/client";
const client = new DivinciClient({ releaseId: "rel_abc123", userToken: jwtFromYourBackend, // obtained from your own auth flow});You can also set or replace the token after construction — useful when the token is fetched asynchronously or rotated:
await client.setToken(newJwt, expiresAtUnixMs);await client.setRefreshToken(refreshJwt); // optionalawait client.logout(); // clears stored tokensHeadless external-user login (no embed script)
Section titled “Headless external-user login (no embed script)”If you’re driving @divinci-ai/client directly (no embed-script.js) and want
to identify an external user against a workspace API key, exchange credentials
in two hops — this is the same flow the embed script performs internally,
documented here for headless/backend-proxied integrations:
1. Backend calls POST /embed/login with the workspace API key and the
caller’s identity. This step must run server-side — it takes your secret API
key.
// Request{ "apikey": "divinci_key_...", "userId": "user_123", "membershipTier": "free", "username": "Ada Lovelace"}
// Response{ "refreshToken": "eyJhbGciOi...", // long-lived (7 days) — NOT a valid Authorization value "refreshExpiresAt": "2026-07-08T00:00:00.000Z", "tierAssigned": "free" // resolved server-side; ignore any tier the caller requested}2. Backend (or a browser-direct client) calls POST /embed/validate-login
with the refresh token plus the calling origin and release, and gets back a
short-lived access token:
// Request{ "refreshToken": "eyJhbGciOi...", "origin": "https://your-app.example.com", "releaseId": "rel_abc123"}
// Response{ "valid": true, "accessToken": "eyJhbGciOi...", // 30-minute lifetime — this is the userToken "userInfo": { "user_id": "...", "nickname": "Ada Lovelace", "picture": null }, "tierConfig": { "tier": "free", "limits": { /* ... */ }, "usage": { /* ... */ }, "remaining": { /* ... */ } }}3. Pass the accessToken as userToken to the client SDK:
const client = new DivinciClient({ releaseId: "rel_abc123", userToken: accessTokenFromValidateLogin,});Troubleshooting { "valid": false }
Section titled “Troubleshooting { "valid": false }”/embed/validate-login deliberately returns a uniform 200 {"valid": false}
for every failure (no oracle for attackers), so check these in order — they
are the real causes, most common first:
- Release ↔ API key whitelabel mismatch. The
releaseIdmust belong to the same whitelabel as the API key that minted the refresh token. A stale release id from an old workspace fails this way even though the token itself is perfectly valid. Verify withGET /white-label-release/{releaseId}— itstargetnames the whitelabel the release belongs to. - Origin not allowlisted.
allowedOriginsis a property of the API key (not the release — you won’t find it on the release object), set at key creation/update in the workspace admin. Theoriginyou send must match an entry exactly (scheme included, case-sensitive). Local dev origins likehttp://localhost:5173must be added explicitly. - Missing fields.
refreshToken,origin, andreleaseIdare all required — omitting any of them fails. - Token/key state. The refresh token is expired (7 days), the API key was deactivated, or its sessions were revoked.
Also note: the membershipTier passed to /embed/login must match one of
the whitelabel’s configured embed-tier slugs (“Invalid tier” 400s list the
availableSlugs). Tiers are defined per whitelabel by the workspace admin —
if your membership levels aren’t in the list, ask the workspace owner to add
matching tier slugs.
2. External user (API key + caller identity)
Section titled “2. External user (API key + caller identity)”For embedded or partner integrations, authenticate with an API key and attach the end user’s identity so usage and quotas attribute to the right person.
const client = new DivinciClient({ releaseId: "rel_abc123", apiKey: "divinci_key_...", externalUser: { userId: "user_123", // required: your system's user id email: "user@example.com", name: "Ada Lovelace", membershipTier: "premium", metadata: { company: "Acme Inc" }, },});
// Or update the identity laterclient.setExternalUser({ userId: "user_456" });3. Custom token storage
Section titled “3. Custom token storage”For full control over where tokens live and how they refresh, implement the
TokenStorage interface and pass it as tokenStorage:
import { DivinciClient, type TokenStorage } from "@divinci-ai/client";
const storage: TokenStorage = { async getToken() { return localStorage.getItem("divinci_token"); }, async setToken(token, expiresAt) { localStorage.setItem("divinci_token", token); }, async clearToken() { localStorage.removeItem("divinci_token"); }, async getRefreshToken() { return localStorage.getItem("divinci_refresh"); }, async setRefreshToken(token) { localStorage.setItem("divinci_refresh", token); },};
const client = new DivinciClient({ releaseId: "rel_abc123", tokenStorage: storage });Checking authentication state
Section titled “Checking authentication state”const signedIn = await client.isAuthenticated();const keyValid = await client.validateApiKey();const keyValid = await divinci.validateApiKey();Security best practices
Section titled “Security best practices”- Never commit secrets — load API keys from environment variables.
- Keep server keys server-side — browsers get user tokens, not API keys.
- Use separate keys per environment — dev, staging, and production.
- Scope keys — issue workspace-scoped keys with
divinci.apiKeys.create()(see API Keys). - Rotate regularly —
divinci.apiKeys.rotate()swaps a key without downtime. - Prefer short-lived user tokens — they limit blast radius if leaked.