SDK

Explanation — what & why

The Vectros SDK is a typed client library for the platform's REST API. Rather than hand-rolling HTTP requests, signing headers, and decoding paginated envelopes, you construct one client and call typed methods grouped by resource: client.records.createRecord(...), client.search.content(...), client.inference.ragInference(...), and so on.

The SDK is generated from the API specification, not written by hand. The spec is the single source of truth: the same definition drives the API gateway, the rendered API reference, and the SDK. That is why the method names, request shapes, and response types in your editor match the API exactly — they are the same artifact, expressed in TypeScript.

The SDK does the mechanical work you would otherwise repeat in every project:

  • Typed requests and responses — your editor autocompletes fields and flags mistakes before you run anything.
  • Streaming as async iteration — chat, RAG, and document-ask endpoints are server-sent-event streams; the SDK exposes them as async iterables you can for await over.
  • A uniform list envelope — list, lookup, and version endpoints return a cursor-paginated envelope (see below) so draining a full result set is one loop, the same every time.

Language coverage — state it honestly

The SDK is generated for Node, Java, and Python, and all three are now exercised end-to-end against staging at parity — each ships a smoke suite (smoke-tests/{,sdk-python/,sdk-java/}) covering the same MVP surface (auth, the list envelope, the error contract, chat/RAG streaming, and cross-context isolation) against the live API, so the call shapes that back this documentation are verified in every language. The snippets here use Node idioms; the Java and Python equivalents follow the same resource grouping and are validated against the same wire contract. See the generated API reference for each language's exact idioms.

The three clients are generated per language, not hand-aligned — so the client class and its constructor differ, and methods follow each language's casing. Don't assume the Node shape transfers verbatim:

LanguageInstallConstruct
Nodenpm install @vectros-ai/sdknew VectrosClient({ token, environment })
Pythonpip install vectrosVectrosApi(base_url=..., token=...)
Javaai.vectros:vectros-sdk (Maven Central)VectrosApiClient.builder().token(...).url(...).build()

Method names mirror the API operations in each language's idiom — Node and Java camelCase (createRecord), Python snake_case (create_record). Runnable Python and Java equivalents of the Node snippets below are in Python and Java.

The list envelope (cursor pagination)

List, lookup, and version endpoints return a uniform envelope:

{ data: T[], nextCursor: string | null }

To drain every page, feed nextCursor back as startFrom until it comes back null:

const ids: string[] = [];
let cursor: string | null | undefined;
do {
  const page = await client.schemas.listSchemas(
    cursor ? { startFrom: cursor, limit: 100 } : { limit: 100 },
  );
  ids.push(...(page.data ?? []).map((s) => s.id!));
  cursor = page.nextCursor;
} while (cursor);

Two endpoints are not enveloped and do not paginate this way:

  • client.search.content(...) returns a results object directly (its hits are on results), with limit/offset for paging.
  • getUsage returns its usage payload directly.

How-to

Install

npm install @vectros-ai/sdk

Construct a client

The client takes a token and a target environment (the API base URL):

import { VectrosClient } from '@vectros-ai/sdk';

const client = new VectrosClient({
  token: process.env.VECTROS_API_KEY!,            // sk_*, ssk_*, or st_*
  environment: 'https://api.vectros.ai',          // staging: https://api.staging.vectros.ai
});

token accepts any of the three credential types — a root key (sk_*), a scoped permanent key (ssk_*), or a short-lived scoped token (st_*). The client makes no distinction; the platform enforces each credential's reach.

Sub-clients are grouped by resource tag:

Sub-clientCovers
client.recordsstructured records
client.schemasrecord/surface schemas
client.documentsdocuments (ingest, upload, retrieve)
client.foldersfolder hierarchy
client.searchunified search
client.inferencechat, RAG, document-ask, model catalog
client.identityusers, identity entities (namespaced — org/client built in, plus any you register), namespaces
client.authping, scoped-token mint, and related auth calls

First call — define a schema, write a record, read it back

// 1. Define a record type. HYBRID indexing = keyword + semantic.
const schema = await client.schemas.createSchema({
  body: {
    typeName: 'patient',
    displayName: 'Patient Record',
    indexMode: 'HYBRID',
    allowedSurfaces: ['record'],
    fields: [
      { fieldId: 'name', fieldType: 'string', required: true, searchable: true },
      { fieldId: 'notes', fieldType: 'string', searchable: true },
      { fieldId: 'department', fieldType: 'string', filterable: true },
      { fieldId: 'email', fieldType: 'string', required: true },
    ],
    lookupFields: [{ fieldName: 'email', unique: true }],
    capabilities: { auditHistory: true },
  },
});

// 2. Write a record. (Synthetic, clearly-fictional data only.)
const record = await client.records.createRecord({
  body: {
    typeName: 'patient',
    schemaId: schema.id!,
    payload: {
      name: 'Jane Doe',
      notes: 'presents with hypertension and complains of chest pain',
      department: 'cardiology',
      email: 'jane.doe@example.test',
    },
  },
});

// 3. Read it back.
const loaded = await client.records.getRecord({ id: record.id! });

Expected result: createRecord returns immediately with indexStatus: 'PENDING_INDEX'; indexing is asynchronous. getRecord returns the stored record, and once indexing completes indexStatus becomes 'INDEXED'.

Search across records and documents

searchContent is unified — it returns both record and document hits unless you narrow with contentTypes. It is not enveloped; read results.

const results = await client.search.content({
  query: 'hypertension chest pain',
  mode: 'HYBRID',          // TEXT | SEMANTIC | HYBRID
  limit: 50,
});
for (const hit of results.results ?? []) {
  console.log(hit.documentId, hit.semanticScore, hit.textScore);
}

Stream a RAG answer

Streaming endpoints are async iterables. Each yielded event has an event discriminator (search_results, content_delta, done, and so on):

const stream = await client.inference.ragInference({
  query: 'What treatment is recommended for stage 1 hypertension?',
  search: { mode: 'HYBRID', limit: 5 },
  maxTokens: 256,
});

let answer = '';
for await (const ev of stream) {
  if (ev.event === 'content_delta') answer += ev.delta ?? '';
  if (ev.event === 'done') console.log('charged:', ev.inferenceBalanceCentsCharged);
}

Plain chat (client.inference.chatInference) and document-scoped ask (client.inference.documentAsk) follow the same async-iterable pattern.

Drain a paginated list

See the envelope loop in the Explanation section above — that is the canonical drain pattern for any list*, lookup*, or get*Versions call.

Update strategies — full replace vs. partial patch (SDK 0.26+)

The default update is a full replace (PUT-style): you send the complete desired body.

await client.records.updateRecord({
  id: record.id!,
  body: {
    typeName: 'patient',
    schemaId: schema.id!,
    payload: { name: 'Jane Doe', notes: 'updated note', department: 'cardiology', email: 'jane.doe@example.test' },
  },
});

A partial patch (RFC-7386 merge-patch) is available on records, documents, and folders with SDK 0.26+: send only the fields you want to change; omitted fields are left untouched, and an explicit null deletes a field.

// (SDK 0.26+) — patch only the notes field.
await client.records.patchRecord({
  id: record.id!,
  body: { payload: { notes: 'patched note' } },
});

Create a record by type name or by schema id (SDK 0.26+)

With SDK 0.26+, createRecord accepts either typeName or schemaId (you no longer need both) — supply whichever you have on hand. Earlier SDK pins expect both; the first-call example above passes both, which works on every version.

Python and Java

The snippets above are Node. The Python (vectros on PyPI) and Java (ai.vectros:vectros-sdk on Maven Central) SDKs are generated from the same spec and cover the same surface; the install, client construction, and method casing differ per language. Each block below is the cross-language twin of the Construct a client + first call (define a schema, write a record, read it back) above.

Python

pip install vectros
import os
import vectros
from vectros import VectrosApi

# Note: the keyword is `base_url` in Python — NOT Node's `environment`.
client = VectrosApi(
    base_url="https://api.vectros.ai",       # staging: https://api.staging.vectros.ai
    token=os.environ["VECTROS_API_KEY"],     # sk_*, ssk_*, or st_*
)

# 1. Define a record type. HYBRID indexing = keyword + semantic.
schema = client.schemas.create_schema(
    type_name="patient",
    display_name="Patient Record",
    index_mode="HYBRID",                     # HYBRID | SEMANTIC | TEXT
    allowed_surfaces=["record"],
    fields=[
        vectros.FieldDef(field_id="name", field_type="string", required=True, searchable=True),
        vectros.FieldDef(field_id="email", field_type="string", required=True),
    ],
    lookup_fields=[vectros.LookupDef(field_name="email", unique=True)],
)

# 2. Write a record. (Synthetic, clearly-fictional data only.)
record = client.records.create_record(
    type_name="patient",
    schema_id=schema.id,
    payload={"name": "Jane Doe", "email": "jane.doe@example.test"},
)

# 3. Read it back.
loaded = client.records.get_record(record.id)

Streaming inference is an iterator of discriminated events (event = content_delta, done, ...), the same shape as Node:

stream = client.inference.chat_inference(
    messages=[vectros.ChatMessage(role="user", content="Say hello.")],
    max_tokens=64,
)
answer = "".join(ev.delta or "" for ev in stream if ev.event == "content_delta")

Sub-clients (client.records, client.schemas, client.documents, client.folders, client.search, client.inference, client.identity, client.auth) and the list envelope (page.data / page.next_cursor) match the Node grouping. API errors raise vectros.core.api_error.ApiError (err.status_code carries the HTTP status).

Java

Maven:

<dependency>
  <groupId>ai.vectros</groupId>
  <artifactId>vectros-sdk</artifactId>
  <version>0.38.0</version>
</dependency>

Gradle:

implementation 'ai.vectros:vectros-sdk:0.38.0'

(The version above is the current published release; confirm the latest on Maven Central.)

import ai.vectros.VectrosApiClient;
import ai.vectros.types.FieldDef;
import ai.vectros.types.FieldDefFieldType;
import ai.vectros.types.LookupDef;
import ai.vectros.types.RecordRequest;
import ai.vectros.types.SchemaRequest;
import ai.vectros.types.SchemaRequestAllowedSurfacesItem;
import ai.vectros.types.SchemaRequestIndexMode;
import java.util.List;
import java.util.Map;

VectrosApiClient client = VectrosApiClient.builder()
    .token(System.getenv("VECTROS_API_KEY"))   // sk_*, ssk_*, or st_*
    .url("https://api.vectros.ai")             // staging: https://api.staging.vectros.ai
    .build();

// 1. Define a record type. HYBRID indexing = keyword + semantic.
var schema = client.schemas().createSchema(SchemaRequest.builder()
    .typeName("patient")
    .displayName("Patient Record")
    .indexMode(SchemaRequestIndexMode.HYBRID)   // HYBRID | SEMANTIC | TEXT
    .allowedSurfaces(List.of(SchemaRequestAllowedSurfacesItem.RECORD))
    .fields(List.of(
        FieldDef.builder().fieldId("name").fieldType(FieldDefFieldType.STRING).required(true).searchable(true).build(),
        FieldDef.builder().fieldId("email").fieldType(FieldDefFieldType.STRING).required(true).build()))
    .lookupFields(List.of(LookupDef.builder().fieldName("email").unique(true).build()))
    .build());

// 2. Write a record. (Synthetic, clearly-fictional data only.)
var record = client.records().createRecord(RecordRequest.builder()
    .typeName("patient")
    .schemaId(schema.getId().orElseThrow())
    .payload(Map.of("name", "Jane Doe", "email", "jane.doe@example.test"))
    .build());

// 3. Read it back.
var loaded = client.records().getRecord(record.getId().orElseThrow());

Java request DTOs live under ai.vectros.types.*; per-endpoint request wrappers (e.g. ChatRequest, ListRecordsRequest) under ai.vectros.resources.<area>.requests.*. Sub-clients are accessor methods (client.records(), client.inference(), ...), responses use Optional getters (record.getId().orElseThrow()), and streaming is an Iterable of discriminated events (event.isContentDelta() / event.getDone()). API errors throw ai.vectros.core.VectrosApiApiException (ex.statusCode()).

Reference

Client construction

OptionTypeNotes
tokenstringsk_*, ssk_*, or st_*. Required.
environmentstringAPI base URL, e.g. https://api.vectros.ai (production) or https://api.staging.vectros.ai (staging). Required.

Sub-clients (by tag)

records, schemas, documents, folders, search, inference, identity, auth. Method names mirror the API operations (createRecord, getRecord, updateRecord, patchRecord, deleteRecord, listRecords, lookupRecords, getRecordVersions, getRecordTombstone, createSchema, ingestDocument, uploadDocument, getDocument, createFolder, chatInference, ragInference, documentAsk, listInferenceModels, createUser, createEntity/listEntities/lookupEntities (namespace-parameterized — org/client are two built-in namespaces among any you register, see identity-access/reference.md), registerNamespace, ping, and so on). The complete, exhaustive list of methods, parameters, and field types is in the generated API reference — link to it by name rather than recreating it here.

Envelope shape

FieldTypeMeaning
dataT[]The page of items.
nextCursorstring | nullPass back as startFrom for the next page; null means no more pages.

Applies to list*, lookup*, and get*Versions. Does not apply to search.content or getUsage.

On list* and lookup*, treat nextCursor as an opaque, encrypted token, not a value to construct: pass back exactly what the previous page returned, never a hand-built or reused startFrom — the API rejects one it did not issue with a 400. A cursor is valid only for the exact query it was issued for. get*Versions shares the same envelope but resumes from a plain row id rather than a sealed cursor — echo nextCursor back the same way, but it is not encrypted, and (unlike list*/lookup*) a caller-supplied id is the expected shape there rather than a rejected one. On every one of these, a page filled to limit is not itself a signal to keep going — the only continue-or-stop signal is whether nextCursor came back null.

Index modes

HYBRID (keyword + semantic, the general-purpose default), SEMANTIC (meaning-based only), TEXT (keyword only). Declared per schema (records) or per document at ingest.

Concurrency

Record and document updates accept an optional expectedVersion in the body for optimistic concurrency; a mismatch is rejected with HTTP 409. Omit it for last-writer-wins.

Version reality (read before pinning)

  • The API spec / SDK is at version 0.38.0 (this figure is sourced from the live release at build time, so it always reflects the current version). The generated Node SDK is a continuous-integration artifact built from that spec — it is not committed to this repository, so you install it from the package registry.
  • Among the in-repo consumers — the CLI, MCP server, and the reference web apps (admin-app, app-vectros-ai) — all currently pin the same SDK release; the React toolkit's dev/test pin tracks it too, though the toolkit itself (an auth/UI library, not a data client) accepts any SDK >=0.9.0 via its peer dependency. These pins are not guaranteed to move together release-to-release — a consumer can be repinned independently of the others — so if you depend on a specific consumer's bundled SDK, check that consumer's own package.json for its exact pin rather than trusting a snapshot figure in this doc.
  • 0.26+ surface — these require SDK 0.26 or newer and are absent on older pins:
    • patchRecord / patchDocument / patchFolder (RFC-7386 merge-patch).
    • createRecord with typeName or schemaId (the either-or form). Passing both works on all versions.
    • All in-repo consumers (CLI, MCP server, and the reference web apps) are past 0.26, so these are reachable everywhere today, including through the reference apps' own UI.
  • 0.27 added an optional lower-cost global-region path for inference (allowGlobalRegion on chat / RAG / document-ask) for tenants entitled to it; see Search and RAG reference.
  • 0.37 added schema lineage (basedOn — a schema customizing another schema sharing its typeName; see data model reference), specificityRank on namespaces, contextId-targeted token minting (root-key-only), userId/scope selectors on document lookup, and externalId/hasMore on search results (see Search and RAG reference). It also replaced the 400 AMBIGUOUS_RECORD_TYPE on a cross-owner typeName collision with ownership- shadowed resolution — see the data model reference for the full basedOn contract.
  • 0.38 added composite (multi-field) lookups, and with it the one compile-breaking change in this release for typed clients: lookupFields[].fieldName on LookupDef is now optional. A lookup declares either fieldName (one field) or the new fieldNames (2–3 fields indexed together, in a fixed declared order). Code that reads fieldName off a schema response must handle it being absent — a multi-field lookup reports fieldNames instead — and a GET-modify-PUT round-trip on a composite lookup must carry fieldNames through, or the update is rejected. Query a composite lookup with a comma-joined field name plus the new values array parameter (alongside the existing singular value); sortFrom/sortTo narrow an exact match to a window of the sort key and, on a composite lookup, need a value for every declared field — a partial (grouped) match has no single ordering to window. A composite lookup can't be unique or rangeEnabled, and only applies to record schemas. RecordLookupResponse was also renamed to RecordLookupPage (SDK type name only — the JSON wire shape is unchanged, so a client reading raw JSON needs no change).
  • 0.38 defines a Vectros-Version request header for pinning a call's response shape to a specific API version. The SDKs do not send it yet — every SDK call falls back to the calling credential's own wire_version_pin (stamped at mint time, not your account's current setting), and from there to the platform floor, regardless of the SDK version you're on. Don't assume the SDK negotiates or pins a wire version today; that lands in a future release.

Notes & limits — what the SDK does NOT do

  • Schemas have no patch. Schema updates are full-replace (PUT) only; there is no patchSchema.
  • Folders cannot be moved or reparented after creation. There is no move operation.
  • There is no identity patch. Users and identity entities update by full replace.
  • search.content and getUsage are not paginated by the cursor envelope — do not look for nextCursor on them.
  • Indexing is asynchronous. A freshly created/ingested item is PENDING_INDEX and not immediately searchable; poll its status (or its appearance in search) before asserting it is queryable.
  • All three SDKs (Node, Java, Python) are smoke-tested end-to-end against staging at parity — see smoke-tests/{,sdk-python/,sdk-java/}. The staging pipeline smokes all three on every change, so a cross-language wire-contract drift surfaces on staging before a release is cut.

Where to go next

  • cli.md — provision schemas, contexts, and scoped keys from the terminal instead of writing this by hand.
  • mcp.md — expose these same operations to an agent as tools.
  • blueprints.md — declare a whole schema set + access profile in one file.
  • The generated API reference — the exhaustive, per-endpoint contract the SDK is generated from.