orbseal/Docs

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.


Related