TypeScript SDK
@naturali/sdk is a typed client for the whole API, generated from the
OpenAPI specs. Every operation, every schema — nothing is
hand-written per endpoint, so the SDK cannot lag the contract.
pnpm add @naturali/sdk
Quick start
import { NaturaliClient } from '@naturali/sdk';
const naturali = new NaturaliClient({
token: process.env.NATURALI_TOKEN,
});
const { data, error } = await naturali.agents.listAgents({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { limit: 10 },
});
if (error) {
console.error(error.error.code); // 'access_denied', 'budget_exceeded', …
} else {
console.log(data.data);
}
A method never throws on a non-2xx response. It resolves to { data, error },
so the error envelope and its
machine-readable code arrive as typed data — the fail-closed codes are values
you branch on, not exceptions you catch.
Client options
The client always talks to https://api.naturali.ai — the only naturali.ai API
origin, and not configurable.
| Option | Purpose |
|---|---|
token | Sent as Authorization: Bearer …. A nat_sk_… API key or a session access JWT from Auth. |
headers | Merged last, so it overrides the header above or adds your own. |
There is no project option. A project-scoped operation names its project in
the path (project_id), and that path parameter is the only thing the API
authorizes against — there is no header notion of a "current project" to keep
in sync with it.
Create one client and reuse it. Two clients never share state, so a request-scoped client per user session is safe.
Call shape
Arguments follow the spec's structure — path, query, body — using the same
snake_case field names as the wire:
await naturali.sessions.addSessionMessage({
path: { project_id, agent_id, session_id },
body: { message: 'What is the capital of France?' },
});
await naturali.knowledge.queryKnowledgeCollection({
path: { project_id, collection_id },
body: { query: 'refund policy', limit: 5 },
});
Method names are the spec's operationIds, so the
API Reference page for an operation names the method that
calls it.
Resources
One property per resource, each mirroring a module:
| Property | Module |
|---|---|
agents | Agents |
apiKeys | API Keys |
auth | Auth |
channels | Channels |
generations | Generations |
knowledge | Knowledge |
models | Models |
projects | Projects |
providers | Providers |
sessions | Sessions |
tools | Tools |
traces | Traces |
naturali.http exposes the underlying HTTP client for interceptors or a one-off
request such as naturali.http.get({ url: '/health' }).
Types
Every schema in the specs is exported:
import type { Agent, AgentCreate, ErrorResponse } from '@naturali/sdk';
created_at and updated_at are handed over as Date objects; every other
field matches the wire shape exactly.
Pagination
List operations are cursor-based. Follow next_cursor until it comes back
null:
let cursor: string | undefined;
do {
const { data } = await naturali.agents.listAgents({
path: { project_id },
query: { limit: 100, cursor },
});
for (const agent of data?.data ?? []) {
console.log(agent.id);
}
cursor = data?.next_cursor ?? undefined;
} while (cursor);
Browser use
Pass a session JWT, never an API key — a nat_sk_… secret does not belong
in a browser (see Auth for the session flow).
Versioning
@naturali/sdk shares its version with the API and with
@naturali/cli — one number, released together. The SDK is generated
from the API's OpenAPI specs, so @naturali/sdk@0.28.0 is the client for API
0.28.0, and installing it never leaves you guessing which contract you are
holding.
A consequence worth knowing: a bump can mean the API gained a resource, not that anything in the SDK's own surface changed. Releases are automatic on merge, so there is no release cadence to follow — pin what you need, or track the latest and read the changelog.