REST API & SDK
JavaScript / TypeScript SDK
npm install @dotlabshq/orbseal-sdk
# peer dep for secret decryption:
npm install libsodium-wrappers
import { ConfigClient } from '@dotlabshq/orbseal-sdk';
const orb = new ConfigClient({
apiKey: process.env.ORBSEAL_API_KEY!,
project: 'taskflow',
environment: 'production',
privateKey: process.env.ORBSEAL_PRIVATE_KEY, // needed for getSecret()
});
const config = await orb.resolve();
const apiUrl = config.get<string>('api_url');
const secret = await config.getSecret('webhook_secret');
// ETag caching — no re-fetch if snapshot unchanged
const fresh = await orb.resolve({ etag: config.etag });
// User-scoped overrides
const userConfig = await orb.resolve({ userId: 'u_abc123' });
REST API
All OrbSeal functionality is also available over a REST API. The base URL is:
https://api.orbseal.com
All requests require a Bearer token:
Authorization: Bearer orb_admin_... | orb_live_...
Authentication
| Token prefix | Used for |
|---|---|
orb_admin_... |
Management: creating workspaces, syncing schema, setting values, cutting releases |
orb_live_... |
Runtime: resolving config, reading/writing user overrides |
Resolve config at runtime
The primary runtime endpoint. Call this when your app starts (or when the cached etag changes).
GET /v1/config/resolve
GET /v1/config/resolve?user=<user_id>
Requires an app key (orb_live_...).
Response:
{
"config": {
"taskflow:theme": "system",
"taskflow:max_seats": 10,
"taskflow:default_view": "board",
"taskflow:app_url": "https://app.taskflow.io"
},
"secrets": {
"taskflow:database_url": "wM2pSljqMNJNrbBAgTo9...",
"taskflow:stripe_secret": "kR2xPQn8YwLmNp4JhTd7..."
},
"resolved_at": "2026-06-06 23:01:08",
"etag": "79a409a6559b9274"
}
Config values are resolved scope-by-scope (user → environment → project → workspace → default). Secret values are ciphertext sealed to your app's public key — decrypt them client-side.
JavaScript / TypeScript
import _sodium from 'libsodium-wrappers';
interface OrbsealConfig {
config: Record<string, unknown>;
secrets: Record<string, string>; // ciphertext
resolved_at: string;
etag: string;
}
export async function resolveConfig(userId?: string): Promise<{
config: Record<string, unknown>;
secrets: Record<string, string>; // decrypted
etag: string;
}> {
const url = 'https://api.orbseal.com/v1/config/resolve'
+ (userId ? `?user=${userId}` : '');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.ORBSEAL_TOKEN}` },
});
if (!res.ok) throw new Error(`orbseal resolve failed: ${res.status}`);
const data: OrbsealConfig = await res.json();
// Decrypt secrets client-side using the app's private key
await _sodium.ready;
const s = _sodium;
const pk = s.from_base64(process.env.ORBSEAL_PUBLIC_KEY!, s.base64_variants.ORIGINAL);
const sk = s.from_base64(process.env.ORBSEAL_PRIVATE_KEY!, s.base64_variants.ORIGINAL);
const decrypted: Record<string, string> = {};
for (const [key, ciphertext] of Object.entries(data.secrets)) {
try {
decrypted[key] = s.to_string(
s.crypto_box_seal_open(s.from_base64(ciphertext, s.base64_variants.ORIGINAL), pk, sk)
);
} catch {
decrypted[key] = ''; // sealed for a different key
}
}
return { config: data.config, secrets: decrypted, etag: data.etag };
}
Caching with etag
let cachedEtag: string | null = null;
let cachedConfig: Awaited<ReturnType<typeof resolveConfig>> | null = null;
export async function getConfig(userId?: string) {
const fresh = await resolveConfig(userId);
if (fresh.etag === cachedEtag && cachedConfig) return cachedConfig;
cachedEtag = fresh.etag;
cachedConfig = fresh;
return fresh;
}
User preferences
Let users update their own preferences without admin tokens. Requires an app key.
Read user overrides
GET /v1/config/user-values?user_id=<id>
Returns all user-scoped, overridable definitions with current values (user override or default).
{
"user_id": "u_abc123",
"settings": [
{
"plugin": "taskflow",
"key": "theme",
"label": "Theme",
"type": "enum",
"enum_values": ["light", "dark", "system"],
"component": "select",
"value": "dark",
"is_override": true,
"default": "system"
}
]
}
Write a user override
PUT /v1/config/user-values/:plugin/:key
{ "value": "dark", "user_id": "u_abc123" }
Reset to default
DELETE /v1/config/user-values/:plugin/:key?user_id=<id>
Schema
List definitions
GET /v1/workspaces/:ws/projects/:proj/schema
Get one definition
GET /v1/workspaces/:ws/projects/:proj/schema/:plugin::key
Sync from orb.yaml
POST /v1/workspaces/:ws/projects/:proj/schema/sync
Content-Type: text/plain
<raw orb.yaml content>
Values
List
GET /v1/workspaces/:ws/projects/:proj/values
Set / update
PUT /v1/workspaces/:ws/projects/:proj/values/:plugin/:key
{
"value": 10,
"scope": "workspace",
"scope_ref": "default"
}
Delete
DELETE /v1/workspaces/:ws/projects/:proj/values/:plugin/:key?scope=workspace&scope_ref=default
Secrets
Seal
PUT /v1/workspaces/:ws/projects/:proj/secrets/:plugin/:key
{
"ciphertext": "<libsodium sealed-box base64>",
"public_key": "<recipient public key base64>",
"scope": "environment",
"scope_ref": "production"
}
The ciphertext must be produced client-side with crypto_box_seal. Orbseal stores only the ciphertext.
Releases
Create
POST /v1/workspaces/:ws/projects/:proj/environments/:env/release
Returns the release payload including config snapshot, version, and etag.
List
GET /v1/workspaces/:ws/projects/:proj/environments/:env/releases
Get specific version
GET /v1/workspaces/:ws/projects/:proj/environments/:env/releases/:version
GET /v1/workspaces/:ws/projects/:proj/environments/:env/releases/latest
Workspaces
GET /v1/workspaces
POST /v1/workspaces { name }
GET /v1/workspaces/:ws
PATCH /v1/workspaces/:ws { name }
DELETE /v1/workspaces/:ws
:ws accepts either slug or short_id.
Projects
GET /v1/workspaces/:ws/projects
POST /v1/workspaces/:ws/projects { name }
GET /v1/workspaces/:ws/projects/:proj
PATCH /v1/workspaces/:ws/projects/:proj { name }
DELETE /v1/workspaces/:ws/projects/:proj
Environments
GET /v1/workspaces/:ws/projects/:proj/environments
POST /v1/workspaces/:ws/projects/:proj/environments { name }
GET /v1/workspaces/:ws/projects/:proj/environments/:env
PATCH /v1/workspaces/:ws/projects/:proj/environments/:env { name }
DELETE /v1/workspaces/:ws/projects/:proj/environments/:env
App keys
GET /v1/workspaces/:ws/projects/:proj/keys
POST /v1/workspaces/:ws/projects/:proj/keys { name, public_key, environment? }
DELETE /v1/workspaces/:ws/projects/:proj/keys/:id
Error format
All errors return JSON:
{
"error": "project not found",
"code": "NOT_FOUND"
}
| HTTP status | Meaning |
|---|---|
400 |
Malformed request |
401 |
Missing or invalid token |
403 |
Token doesn't have permission for this operation |
404 |
Resource not found |
422 |
Validation error (see code for details) |
500 |
Internal server error |
Rate limits
| Token type | Rate limit |
|---|---|
| Admin token | 300 req / min |
| App key | 600 req / min |
Headers returned: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.