The official JavaScript/TypeScript SDK for the Seclai API. Provides full typed coverage of all API endpoints, file uploads, SSE streaming, polling helpers, and automatic pagination.
Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and any runtime with a fetch implementation.
npm install @seclai/sdkimport { Seclai } from "@seclai/sdk";
const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
// List all sources
const sources = await client.listSources();
// Run an agent and stream the result
const result = await client.runStreamingAgentAndWait(
"agent_id",
{ input: "Summarize the latest uploads", metadata: {} },
{ timeoutMs: 120_000 },
);
console.log(result);| Option | Environment variable | Default |
|---|---|---|
apiKey |
SECLAI_API_KEY |
— |
accessToken |
— | — |
profile |
SECLAI_PROFILE |
"default" |
configDir |
SECLAI_CONFIG_DIR |
~/.seclai |
autoRefresh |
— | true |
accountId |
— | — |
baseUrl |
SECLAI_API_URL |
https://api.seclai.com |
apiKeyHeader |
— | x-api-key |
defaultHeaders |
— | {} |
fetch |
— | globalThis.fetch |
Credentials are resolved via a chain (first match wins):
- Explicit
apiKeyoption - Explicit
accessTokenoption (string or() => string | Promise<string>) SECLAI_API_KEYenvironment variable- SSO — cached tokens from
~/.seclai/sso/cache/(requires a priorseclai auth login)
// API key
const client = new Seclai({ apiKey: "sk-..." });// Static bearer token
const client = new Seclai({ accessToken: "eyJhbGciOi..." });// Dynamic bearer token provider (called per request)
const client = new Seclai({
accessToken: async () => fetchTokenFromVault(),
});// SSO profile (uses cached tokens, auto-refreshes)
const client = new Seclai({ profile: "my-profile" });// Environment variable (no options needed)
// export SECLAI_API_KEY="sk-..."
const client = new Seclai();SSO is the default fallback when no explicit credentials are provided. The SDK
includes built-in production SSO defaults, so seclai configure sso is not
required. You only need to log in once to populate the token cache:
npx @seclai/cli auth login # authenticate via browser — no prior setup neededTo customize SSO settings (e.g. for a staging environment), use seclai configure sso
or set environment variables:
| Variable | Description | Default |
|---|---|---|
SECLAI_SSO_DOMAIN |
Cognito domain | auth.seclai.com |
SECLAI_SSO_CLIENT_ID |
Cognito app client ID | 4bgf8v9qmc5puivbaqon9n5lmr |
SECLAI_SSO_REGION |
AWS region | us-west-2 |
Online API documentation (latest):
https://seclai.github.io/seclai-javascript/latest/
Release history is in CHANGELOG.md.
The API dates its backward-incompatible changes. Nothing changes for you until you opt in, either per client or by pinning the account:
const client = new Seclai({
apiKey: "...",
apiVersion: SeclaiApiVersion.V2026_07_27, // sent as the Seclai-Version header
});
const state = await client.getApiVersion(); // what this request resolved to
await client.updateApiVersion(SeclaiApiVersion.V2026_07_27); // pin the whole accountLeave apiVersion unset and the header is omitted, so the account's pinned
baseline applies and responses keep their current shapes. Upgrading this package
alone never changes the wire contract.
Known versions are on SeclaiApiVersion (V2026_07_01, V2026_07_27, plus
Default and Latest), imported from @seclai/sdk. A version this release was
not built against throws at construction: a newer version can reshape
responses, and this client would decode them incorrectly rather than reject them.
Upgrade the package to adopt a new version, or set allowUnknownApiVersion if
you have to move first and accept that risk.
The guard only covers the header. An account pinned server-side can still be
newer than this release — getApiVersion() reports the effective_version the
request resolved to, and comparing it against SeclaiApiVersion.Latest is how
you detect the gap.
What 2026-07-27 changes. Undeclared query parameters become a 422 instead
of being ignored, and list endpoints move to the canonical {data, pagination}
envelope. The affected methods read both shapes, so they keep working either way
— but the metadata moves:
| Method | Before | From 2026-07-27 |
|---|---|---|
listEvaluationCriteriaPage() |
bare array | data + pagination |
listRunEvaluationResults() |
bare array | data + pagination |
listAlertConfigs() |
configs + total |
data + pagination |
listModelAlerts() |
alerts + total |
data + pagination |
Prefer pagination over the flat total/page/limit properties, and read the
last two with res.data ?? res.configs / res.data ?? res.alerts. The legacy
keys will be deprecated and then removed once the canonical envelope is the
default.
Discover who the credential belongs to and which organizations it can act for.
Each organization's account_id is what you pass as the client's accountId
option (sent as the X-Account-Id header) to target that org context.
const me = await client.getMe();
console.log(me.account_id); // the user's personal account
for (const org of me.organizations) {
console.log(org.name, org.account_id);
}
// Act as an organization
const orgClient = new Seclai({ accountId: me.organizations[0].account_id });// CRUD
const agents = await client.listAgents({ page: 1, limit: 20 });
const agent = await client.createAgent({ name: "My Agent", description: "..." });
const fetched = await client.getAgent("agent_id");
const updated = await client.updateAgent("agent_id", { name: "Renamed" });
await client.deleteAgent("agent_id");
// Pause / resume — a disabled agent stops firing from every trigger path
const callers = await client.getAgentCallers("agent_id"); // live agents calling this one
await client.disableAgent("agent_id"); // 409 if any caller above is still live
await client.enableAgent("agent_id");
// Definition (step workflow)
const def = await client.getAgentDefinition("agent_id");
await client.updateAgentDefinition("agent_id", { steps: [...], change_id: def.change_id });
// Export / import an agent
const exported = await client.exportAgent("agent_id");
// Validate the payload first to surface unresolved entity refs in this account
const preview = await client.previewImportAgent({ agent_definition: exported });
const unresolved = (preview.unresolved_refs ?? []) as Array<{ ref_id: string }>;
const entity_remap = Object.fromEntries(
unresolved.map((ref) => [ref.ref_id, /* picked target uuid */ ""]),
);
// Commit — `entity_remap` substitutes workflow refs before save
const imported = await client.createAgent({
name: "Imported",
trigger_type: "dynamic_input",
agent_definition: exported,
entity_remap,
});
// `imported.import_warnings` lists any items that couldn't be applied.// Start a run
const run = await client.runAgent("agent_id", { input: "Hello" });
// List & search runs
const runs = await client.listAgentRuns("agent_id", { status: "completed" });
const search = await client.searchAgentRuns({ agent_id: "...", status: ["completed"] });
// Fetch run details (optionally with step outputs)
const detail = await client.getAgentRun("run_id", { includeStepOutputs: true });
// Cancel or delete
await client.cancelAgentRun("run_id");
await client.deleteAgentRun("run_id");The SDK provides two streaming patterns over the SSE /runs/stream endpoint:
Block until done — returns the final done payload or throws on timeout:
const result = await client.runStreamingAgentAndWait(
"agent_id",
{ input: "Hello", metadata: {} },
{ timeoutMs: 60_000 },
);Async iterator — yields every SSE event as { event, data }:
for await (const event of client.runStreamingAgent(
"agent_id",
{ input: "Hello" },
{ timeoutMs: 120_000 },
)) {
console.log(event.event, event.data);
if (event.event === "done") break;
}For environments where SSE is not practical, poll for a completed run:
const result = await client.runAgentAndPoll(
"agent_id",
{ input: "Hello" },
{ pollIntervalMs: 2_000, timeoutMs: 120_000 },
);// Discover which files (if any) the agent expects before staging uploads
const refs = await client.getAgentAttachmentReferences("agent_id");
if (refs.requires_uploads) {
// refs.agent lists the exact_names / indexes_max / patterns a run batch must satisfy
}
const upload = await client.uploadAgentInput("agent_id", {
file: new Uint8Array([...]),
fileName: "input.pdf",
});
const status = await client.getAgentInputUploadStatus("agent_id", upload.upload_id);// Download a file emitted by a step in an agent run. The attachment_id is the
// URL-safe-base64 storage_key surfaced in run output manifests / webhooks.
const resp = await client.downloadAgentRunAttachment("run_id", "attachment_id");
const blob = await resp.blob(); // raw Response — stream or save the bytesConfigure the inbound address and handling rules on an agent's EMAIL_RECEIVED
trigger. Omitted fields are left unchanged; null (or "" for alias) clears them.
const config = await client.setEmailTriggerConfig("agent_id", "trigger_id", {
alias: "support",
allowed_senders: ["example.com", "ops@partner.com"], // empty/null accepts any sender
ignore_auto_generated: true, // drop auto-replies/bulk mail to prevent loops
require_sender_auth: true, // require SPF or DMARC even on an open inbox
queue_on_quota: false, // park over-rate mail instead of failing it
});
console.log(config.email_addresses); // ["support.<accountID>@agent.seclai.com", ...]// Recipients who opted out of this account's agent emails
const optOuts = await client.listAgentEmailOptOuts({ agentId: "agent_id", limit: 50 });
await client.removeAgentEmailOptOut("optout_id"); // opt them back in
// Blocked inbound senders (owner/admin only)
const blocked = await client.listBlockedEmailSenders({ limit: 50, offset: 0 });
await client.blockEmailSender({ sender_email: "spam.example.com", match_type: "domain" });
await client.unblockEmailSender("blocked_id");
// Auto-block on a governance BLOCK: "disabled" | "input" | "input_and_output"
await client.setAutoBlockMode({ mode: "input_and_output" });
// Inbound emails discarded before running an agent
const rejections = await client.listInboundEmailRejections({ agentId: "agent_id" });
// Account-wide overload circuit breaker
const status = await client.getInboundEmailStatus(); // { paused, queued_backlog }
await client.cancelQueuedEmailRuns(); // fail all QUEUED (over-quota parked) runs
await client.resumeInboundEmail(); // one-shot override; re-arms if still overloadedconst steps = await client.generateAgentSteps("agent_id", { user_input: "Build a RAG pipeline" });
const config = await client.generateStepConfig("agent_id", { step_type: "llm", user_input: "..." });
// Conversation history
const history = await client.getAgentAiConversationHistory("agent_id");
await client.markAgentAiSuggestion("agent_id", "conversation_id", { accepted: true });const criteria = await client.listEvaluationCriteria("agent_id", { page: 1, limit: 50 });
// page/limit only take effect with apiVersion "2026-07-27" or later; the legacy
// response is unpaginated. listEvaluationCriteriaPage() returns the same items
// plus a `pagination` object when opted in.
const created = await client.createEvaluationCriteria("agent_id", { name: "Accuracy", ... });
const detail = await client.getEvaluationCriteria("criteria_id");
await client.updateEvaluationCriteria("criteria_id", { ... });
await client.deleteEvaluationCriteria("criteria_id");
// Test a draft
await client.testDraftEvaluation("agent_id", { criteria: { ... }, run_id: "..." });
// Results by criteria
const results = await client.listEvaluationResults("criteria_id");
const summary = await client.getEvaluationCriteriaSummary("criteria_id");
await client.createEvaluationResult("criteria_id", { ... });
// Results by run
const runResults = await client.listRunEvaluationResults("agent_id", "run_id");
// Non-manual evaluation summary
const nonManual = await client.getNonManualEvaluationSummary("agent_id");
// Compatible runs for a criteria
const compatible = await client.listCompatibleRuns("criteria_id");const kbs = await client.listKnowledgeBases();
const kb = await client.createKnowledgeBase({ name: "Docs KB" });
const fetched = await client.getKnowledgeBase("kb_id");
await client.updateKnowledgeBase("kb_id", { name: "Renamed KB" });
await client.deleteKnowledgeBase("kb_id");const banks = await client.listMemoryBanks();
const bank = await client.createMemoryBank({ name: "Chat Memory", type: "conversation" });
const fetched = await client.getMemoryBank("mb_id");
await client.updateMemoryBank("mb_id", { name: "Renamed" });
await client.deleteMemoryBank("mb_id");
// Stats & compaction
const stats = await client.getMemoryBankStats("mb_id");
await client.compactMemoryBank("mb_id");
// Test compaction
const test = await client.testMemoryBankCompaction("mb_id", { ... });
const standalone = await client.testCompactionPromptStandalone({ ... });
// Templates & agents
const templates = await client.listMemoryBankTemplates();
const agents = await client.getAgentsUsingMemoryBank("mb_id");
// AI assistant
const suggestion = await client.generateMemoryBankConfig({ user_input: "..." });
const history = await client.getMemoryBankAiLastConversation();
await client.acceptMemoryBankAiSuggestion("conv_id", { ... });
// Source management
await client.deleteMemoryBankSource("mb_id");const sources = await client.listSources({ page: 1, limit: 20, order: "asc" });
const source = await client.createSource({ name: "My Source", ... });
const fetched = await client.getSource("source_id");
await client.updateSource("source_id", { name: "Renamed" });
await client.deleteSource("source_id");Upload a file to a source (max 200 MiB). The SDK infers MIME type from the file extension when mimeType is not provided.
import { readFile } from "node:fs/promises";
await client.uploadFileToSource("source_id", {
file: await readFile("document.pdf"),
fileName: "document.pdf",
title: "Q4 Report",
metadata: { department: "finance" },
});Upload inline text:
await client.uploadInlineTextToSource("source_id", {
text: "Hello, world!",
title: "Greeting",
});Replace a content version with a new file:
await client.uploadFileToContent("content_version_id", {
file: await readFile("updated.pdf"),
fileName: "updated.pdf",
mimeType: "application/pdf",
});const exports = await client.listSourceExports("source_id");
const exp = await client.createSourceExport("source_id", { format: "json" });
const status = await client.getSourceExport("source_id", exp.id);
const estimate = await client.estimateSourceExport("source_id", {});
const response = await client.downloadSourceExport("source_id", exp.id);
await client.deleteSourceExport("source_id", exp.id);
await client.cancelSourceExport("source_id", exp.id);const migration = await client.getSourceEmbeddingMigration("source_id");
await client.startSourceEmbeddingMigration("source_id", { target_model: "..." });
await client.cancelSourceEmbeddingMigration("source_id");const detail = await client.getContentDetail("content_id", { start: 0, end: 1000 });
const embeddings = await client.listContentEmbeddings("content_id");
await client.deleteContent("content_id");
// Replace content with inline text
await client.replaceContentWithInlineText("content_id", { text: "Updated text", title: "Updated" });
// Upload a replacement file
await client.uploadFileToContent("content_id", {
file: await readFile("updated.pdf"),
fileName: "updated.pdf",
});const solutions = await client.listSolutions();
const sol = await client.createSolution({ name: "My Solution" });
const fetched = await client.getSolution("solution_id");
await client.updateSolution("solution_id", { name: "Renamed" });
await client.deleteSolution("solution_id");
// Link / unlink resources
await client.linkAgentsToSolution("solution_id", { ids: ["agent_id"] });
await client.unlinkAgentsFromSolution("solution_id", { ids: ["agent_id"] });
await client.linkKnowledgeBasesToSolution("solution_id", { ids: ["kb_id"] });
await client.unlinkKnowledgeBasesFromSolution("solution_id", { ids: ["kb_id"] });
await client.linkSourceConnectionsToSolution("solution_id", { ids: ["source_id"] });
await client.unlinkSourceConnectionsFromSolution("solution_id", { ids: ["source_id"] });
// AI assistant
const plan = await client.generateSolutionAiPlan("solution_id", { user_input: "Set up a RAG pipeline" });
await client.acceptSolutionAiPlan("solution_id", "conversation_id", {});
await client.declineSolutionAiPlan("solution_id", "conversation_id");
// AI-generated knowledge base / source within the solution
await client.generateSolutionAiKnowledgeBase("solution_id", { user_input: "..." });
await client.generateSolutionAiSource("solution_id", { user_input: "..." });
// Conversations
const convs = await client.listSolutionConversations("solution_id");
await client.addSolutionConversationTurn("solution_id", { user_input: "..." });
await client.markSolutionConversationTurn("solution_id", "conversation_id", { ... });const plan = await client.generateGovernanceAiPlan({ user_input: "Add a toxicity policy" });
const convs = await client.listGovernanceAiConversations();
await client.acceptGovernanceAiPlan("conversation_id");
await client.declineGovernanceAiPlan("conversation_id");const alerts = await client.listAlerts({ status: "active" });
const alert = await client.getAlert("alert_id");
await client.changeAlertStatus("alert_id", { status: "resolved" });
await client.addAlertComment("alert_id", { text: "Investigating" });
// Subscriptions
await client.subscribeToAlert("alert_id");
await client.unsubscribeFromAlert("alert_id");
// Alert configs
const configs = await client.listAlertConfigs();
await client.createAlertConfig({ ... });
await client.getAlertConfig("config_id");
await client.updateAlertConfig("config_id", { ... });
await client.deleteAlertConfig("config_id");
// Organization preferences
const prefs = await client.listOrganizationAlertPreferences();
await client.updateOrganizationAlertPreference("org_id", "alert_type", { ... });// List models, optionally filtered by capability
const providers = await client.listModels({ supportsToolUse: true });
const imageModels = await client.listModels({ supportsOutputMedia: "image" });
const pdfModels = await client.listModels({ supportsInputMedia: "pdf" });
const model = await client.getModel("model_id");
// Media-generation quality tiers (fast/balanced/thorough) and what each resolves to
const tiers = await client.getGenerationTiers();
const alerts = await client.listModelAlerts();
await client.markModelAlertRead("alert_id");
await client.markAllModelAlertsRead();
const unread = await client.getUnreadModelAlertCount();
const recs = await client.getModelRecommendations("model_id");
// Model playground experiments
const experiment = await client.createExperiment({ model_ids: ["model_id"], prompt: "..." });
const experiments = await client.listExperiments();
const detail = await client.getExperiment("experiment_id");
await client.cancelExperiment("experiment_id");
await client.deleteExperiment("experiment_id"); // soft-delete, preserves audit historyconst results = await client.search({ query: "quarterly report" });
const filtered = await client.search({ query: "my agent", entityType: "agent", limit: 5 });Search the Seclai docs. Results are global (not account-scoped) and each carries a
doc_slug plus an optional anchor for building a
https://seclai.com/docs/<doc_slug>[#<anchor>] link.
// Fast title/summary match, no AI cost
const hits = await client.searchDocs({ query: "email triggers" });
// Semantic body match — adds a `highlight` with the best matching sentence
const semantic = await client.searchDocs({
query: "how do I stop auto-reply loops",
mode: "semantic",
limit: 5,
});Send and receive agent email on your own domain instead of the shared
agent.seclai.com. These endpoints require a user-bound credential (an
account-only API key is refused with 403) and, for mutations, an account owner/admin.
// Current domains, their DNS records, and what your plan allows
const { domains, can_add_custom, has_custom } = await client.listEmailDomains();
// Add a vanity subdomain (<slug>.seclai.com) or your own custom domain
const vanity = await client.addEmailDomain({ kind: "vanity", value: "acme" });
const custom = await client.addEmailDomain({
kind: "custom",
value: "agent.mycompany.com",
delegated: true, // let Seclai manage a dedicated Route53 zone
});
// Publish custom.dns_records at your DNS provider, then check without waiting
// for the background sweep
const checked = await client.verifyEmailDomain(custom.id);
// Promote a verified domain, or fall back to the shared domain
await client.setPrimaryEmailDomain(custom.id);
await client.useSharedEmailDomain(); // keeps the domain configured & verified
// Confirm delivery end-to-end (always sends to the account owner only)
await client.sendEmailDomainTestEmail(custom.id);
// DMARC aggregate-report summary
const dmarc = await client.getDmarcSummary(custom.id, { days: 30, topSources: 10 });
// Removing a delegated domain returns a cleanup_note about the registrar NS record
const { cleanup_note } = await client.removeEmailDomain(custom.id);// Generate plans for different resource types
const kb = await client.aiAssistantKnowledgeBase({ user_input: "Create a docs KB" });
const src = await client.aiAssistantSource({ user_input: "Add a web source" });
const sol = await client.aiAssistantSolution({ user_input: "Set up monitoring" });
const mb = await client.aiAssistantMemoryBank({ user_input: "Create a chat memory" });
// Accept or decline the generated plan
await client.acceptAiAssistantPlan("conversation_id", { confirm_deletions: true });
await client.declineAiAssistantPlan("conversation_id");
// Memory bank conversation history
const history = await client.getAiAssistantMemoryBankHistory();
await client.acceptAiMemoryBankSuggestion("conversation_id", { ... });
// Feedback
await client.submitAiFeedback({ ... });Automatically iterate through all pages:
for await (const source of client.paginate(
(opts) => client.listSources(opts),
{ limit: 50 },
)) {
console.log(source.name);
}All errors extend SeclaiError:
import {
SeclaiError,
SeclaiConfigurationError,
SeclaiAPIStatusError,
SeclaiAPIValidationError,
SeclaiStreamingError,
} from "@seclai/sdk";
try {
await client.getAgent("bad_id");
} catch (err) {
if (err instanceof SeclaiAPIValidationError) {
console.error("Validation:", err.validationError);
} else if (err instanceof SeclaiAPIStatusError) {
console.error(`HTTP ${err.statusCode}:`, err.responseText);
} else if (err instanceof SeclaiStreamingError) {
console.error("Stream failed for run:", err.runId);
}
}All low-level methods support an AbortSignal for request cancellation:
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5_000);
const data = await client.request("GET", "/agents", {
signal: controller.signal,
});For endpoints not yet covered by a convenience method, use request or requestRaw:
// JSON request/response
const data = await client.request("POST", "/custom/endpoint", {
json: { key: "value" },
query: { filter: "active" },
});
// Raw Response (e.g. binary downloads)
const response = await client.requestRaw("GET", "/files/download/123");
const blob = await response.blob();npm installnpm run typechecknpm run buildThis also regenerates src/openapi.ts from openapi/seclai.openapi.json.
npm testGenerate HTML docs into build/docs/:
npm run docs