Skip to content

JavaScript/TypeScript SDK ​

JavaScript/TypeScript SDK for the Creed Space tool-call gateway. Promise-based, ships with type definitions. Before your agent runs a tool, the SDK asks for a decision on the exact tool name and arguments. An allow comes back with a short-lived signed token that your code redeems once with authorize.

Installation ​

bash
npm install @creedspace/sdk
# or
yarn add @creedspace/sdk
# or
pnpm add @creedspace/sdk

Version

This page describes @creedspace/sdk 3.1.0, which is not published: npm has 1.0.0, which predates the authorize binding and the error classes shown below.

The SDK does not cover the content endpoints (/api/v1/safety/evaluate, /api/v1/pdp/adjudicate, constitutions). Call those over REST with a cs_ key in the X-API-Key header; see the Quickstart.

Quick Start ​

typescript
import { createClient } from '@creedspace/sdk';

const client = createClient({ apiKey: 'crd_live_...' });

const result = await client.decide({
    toolName: 'send_email',
    arguments: { to: 'user@example.com', subject: 'Hello' }
});

console.log(`Decision: ${result.decision}`);    // ALLOW, DENY, REQUIRE_HUMAN
console.log(`Risk score: ${result.risk.score}`); // 0.0 - 1.0

Configuration ​

typescript
import { createClient } from '@creedspace/sdk';

const client = createClient({
    apiKey: 'crd_live_...',
    baseUrl: 'https://api.creed.space', // Optional
    timeoutMs: 30000, // Request timeout in ms
    fetch: globalThis.fetch // Optional: pass your own fetch implementation
});

The key must be a gateway key (crd_live_... or crd_test_...), which the SDK sends as Authorization: Bearer. A key in any other format throws CreedError with code INVALID_API_KEY when the client is created. See Authentication.

Environment Variables ​

typescript
const client = createClient({
    apiKey: process.env.CREED_GATEWAY_KEY!
});

Decisions ​

Callbacks ​

typescript
const result = await client.decide({
    toolName: 'send_email',
    arguments: { to: 'user@example.com', subject: 'Hello' },
    onAllow: (decision) => console.log('Decision token:', decision.decisionToken),
    onDeny: (decision) => console.log('Denied:', decision.reasons),
    onRequireHuman: (decision) => console.log('Review queued:', decision.reviewId)
});

constitutionId (default 'default') selects the constitution to evaluate against. context is optional; the server takes the tenant from your key and ignores any tenant, user or session ID sent there.

Result Types ​

typescript
type DecideResult = AllowDecision | DenyDecision | RequireHumanDecision;

// Shared fields: decision, runId, actionId, toolCallId, argsHash, risk { score, labels }
interface AllowDecision        { decision: 'ALLOW'; decisionToken: string; expiresAt: string; /* ... */ }
interface DenyDecision         { decision: 'DENY'; reasons: string[]; guidance: Record<string, string>; /* ... */ }
interface RequireHumanDecision { decision: 'REQUIRE_HUMAN'; reviewId?: string; featureStatus: 'active'; /* ... */ }

Response keys are converted from snake_case to camelCase.

Authorize Before Running the Tool ​

An ALLOW is not yet permission to run the tool. Redeem the token once with authorize, passing back the identifiers decide returned, and start the side effect only if authorized is true:

typescript
if (result.decision === 'ALLOW') {
    const auth = await client.authorize({
        decisionToken: result.decisionToken,
        runId: result.runId,
        actionId: result.actionId,
        toolCallId: result.toolCallId,
        projectId: 'default',
        toolName: 'send_email',
        argsHash: result.argsHash
    });
    if (auth.authorized) {
        // run the tool now
    }
}

SDK decisions are always made under project 'default'. A successful authorize authorizes starting the side effect at that instant; it cannot cancel work your code has already started.

Human Review ​

typescript
if (result.decision === 'REQUIRE_HUMAN' && result.reviewId) {
    const status = await client.reviewStatus(result.reviewId);
    console.log(status.status); // pending, approved, denied, expired
}

Audit Trail ​

typescript
const audit = await client.audit({ runId: result.runId });

console.log(audit.eventCount);
console.log(audit.integrity.verified); // the server's own re-verification of the hash chain

audit also takes actionId and limit (default 50).

Service Status ​

typescript
const status = await client.status();
console.log(status.service, status.version);

Error Handling ​

typescript
import {
    createClient,
    CreedError,
    AuthenticationError,
    RateLimitError,
    ServerError
} from '@creedspace/sdk';

const client = createClient({ apiKey: process.env.CREED_GATEWAY_KEY! });

try {
    const result = await client.decide({ toolName: 'send_email', arguments: { to: 'ops@example.com' } });
} catch (error) {
    if (error instanceof AuthenticationError) {
        console.log('Invalid gateway key');
    } else if (error instanceof RateLimitError) {
        console.log(`Rate limited. Retry after ${error.retryAfter}s`);
    } else if (error instanceof ServerError) {
        console.log(`Server error ${error.statusCode}: ${error.message}`);
    } else if (error instanceof CreedError) {
        console.log(`API error (${error.code}): ${error.message}`);
    }
}

Timeouts and network failures throw CreedError with code TIMEOUT or NETWORK_ERROR.

TypeScript Support ​

The SDK ships with TypeScript types for every request and response:

typescript
import type { DecideResult } from '@creedspace/sdk';

function handle(result: DecideResult): void {
    if (result.decision === 'ALLOW') {
        console.log(result.decisionToken);
    } else if (result.risk.score > 0.5) {
        console.log('High risk tool call');
    }
}

Examples ​

Gating a Tool Call ​

typescript
import { createClient } from '@creedspace/sdk';

const client = createClient({ apiKey: process.env.CREED_GATEWAY_KEY! });

async function runGuarded(toolName: string, args: Record<string, unknown>, run: () => Promise<unknown>) {
    const decision = await client.decide({ toolName, arguments: args });

    if (decision.decision === 'DENY') {
        throw new Error(`Denied: ${decision.reasons.join(', ')}`);
    }
    if (decision.decision === 'REQUIRE_HUMAN') {
        return { status: 'pending_review', reviewId: decision.reviewId };
    }

    const auth = await client.authorize({
        decisionToken: decision.decisionToken,
        runId: decision.runId,
        actionId: decision.actionId,
        toolCallId: decision.toolCallId,
        projectId: 'default',
        toolName,
        argsHash: decision.argsHash
    });
    if (!auth.authorized) {
        throw new Error(`Not authorized: ${auth.message}`);
    }
    return run();
}

Browser Usage ​

In the browser, proxy requests through your server so the API key stays server-side:

typescript
// Server-side proxy
app.post('/api/tool-decision', async (req, res) => {
    const result = await client.decide({ toolName: req.body.toolName, arguments: req.body.arguments });
    res.json({ decision: result.decision });
});

// Client-side
const response = await fetch('/api/tool-decision', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ toolName: 'send_email', arguments: { to: 'user@example.com' } })
});
const result = await response.json();

WARNING

Never expose your API key in client-side code. Always use a server-side proxy.

REST Content Endpoints ​

Safety Evaluation and PDP Adjudication use a cs_ key in X-API-Key, separate from the gateway SDK. Keep this helper on your server.

REST Helper ​

No package is needed. Keep your API key on the server and send it in the X-API-Key header:

typescript
// creed.ts (server-side only)
const CREED_BASE_URL = 'https://api.creed.space';

export async function creedRequest<T>(path: string, body?: unknown): Promise<T> {
    const response = await fetch(`${CREED_BASE_URL}${path}`, {
        method: body === undefined ? 'GET' : 'POST',
        headers: {
            'X-API-Key': process.env.CREED_API_KEY!,
            'Content-Type': 'application/json'
        },
        body: body === undefined ? undefined : JSON.stringify(body),
        signal: AbortSignal.timeout(30_000) // request timeout in ms
    });
    if (!response.ok) {
        throw new CreedApiError(response.status, response.headers.get('Retry-After'), await response.text());
    }
    return (await response.json()) as T;
}

export class CreedApiError extends Error {
    constructor(
        public readonly status: number,
        public readonly retryAfter: string | null,
        body: string
    ) {
        super(`Creed Space API error ${status}: ${body}`);
    }
}

Safety Evaluation ​

POST /api/v1/safety/evaluate scores text against the Psychopathia agent-dysfunction rule set. It is not a general harmful-content classifier; read the Safety Evaluation reference for what it does and does not catch.

Basic Evaluation ​

typescript
const { evaluation } = await creedRequest<EvaluateResponse>('/api/v1/safety/evaluate', {
    input_text: 'User message here'
});

if (evaluation.decision === 'allow') {
    processMessage(message);
} else {
    // 'modify', 'block' or 'escalate': hold the text back from the agent.
    holdForReview(message, evaluation);
}

With Context ​

Only four boolean flags in context change the score: is_documentation, is_test_file, is_tutorial and read_only. Other keys are ignored. The flags lower the score, so set them from your own code and never from user input:

typescript
const { evaluation } = await creedRequest<EvaluateResponse>('/api/v1/safety/evaluate', {
    input_text: tutorialPageText,
    context: { is_tutorial: true }, // set by our crawler, not by the user
    session_id: 'user-123-session-456'
});

Response Type ​

typescript
interface EvaluateResponse {
    status: 'success';
    scope: string;
    crisis_support: {
        detected: true;
        message: string;
        resources: Array<{ name: string; region: string; contact: string }>;
    } | null;
    evaluation: {
        risk_score: number; // 0.0 - 1.0
        decision: 'allow' | 'modify' | 'block' | 'escalate';
        is_blocked: boolean;
        matched_rules: Array<{ id: string; name: string; axis: string; severity: number }>;
        context_multiplier: number;
        explanations: string[];
        decision_time_ms: number;
    };
}

PDP Adjudication ​

For policy decisions against your constitutions, with rationale, norm citations and a verifiable decision kernel:

typescript
const kernel = await creedRequest<DecisionKernel>('/api/v1/pdp/adjudicate', {
    message: 'Content to evaluate',
    constitutions: ['UEF'],
    context: { key: 'value' }
});

console.log(`Decision: ${kernel.decision}`); // permit | divert | forbid | depends
console.log(`Rationale: ${kernel.rationale}`);
console.log('Norms:', kernel.norms);

if (kernel.decision !== 'permit') {
    // divert, forbid and depends are all "not permitted"
}
typescript
interface DecisionKernel {
    decision: 'permit' | 'divert' | 'forbid' | 'depends';
    rationale: string;
    norms: Array<{ t: 'O' | 'F' | 'P'; act: string; ref: string }>;
    precedence: string[];
    caveats: string[];
    nonce: string | null;
    hash: string;
    timestamp: number;
    decision_id?: string | null;
}

See the PDP reference for every field.

Constitutions ​

List Available Constitutions ​

typescript
const hierarchy = await creedRequest<{
    rootFolders: Array<{
        folderTitle: string;
        constitutions: Array<{ title: string; relativePath: string }>;
    }>;
}>('/api/v1/constitutions/');

for (const folder of hierarchy.rootFolders) {
    for (const c of folder.constitutions) {
        console.log(`${folder.folderTitle}: ${c.title} (${c.relativePath})`);
    }
}

Creed Space API documentation. Privacy, Terms and Compliance live on creed.space.