Skip to main content
SDK

TypeScript SDK

Install and use the Qoder Cloud Agents TypeScript SDK, including types, asynchronous calls, streaming responses, cancellation, pagination, and custom fetch.

The Qoder Cloud Agents TypeScript SDK works with TypeScript and JavaScript applications. It provides Forward and Managed clients, request and response types, asynchronous pagination, and SSE streaming. The package offers both ES Modules and CommonJS entry points.

Installation

Requires Node.js 20.12 or later:
npm install qca-sdk
Keep the package version and lockfile in version control. The examples in this document run on a Node.js server.

Basic usage

Save your personal access token (PAT) in an environment variable:
export QODER_PAT="replace-with-your-PAT"
Use the Forward client to create a template and identity, start a session, and send a message. Save the following code as an .mjs file to run it directly, or use it in a TypeScript project supporting ES Modules:
import { ForwardClient } from 'qca-sdk';

const client = new ForwardClient();

const env = await client.environments.create({
  name: 'demo',
  config: { type: 'cloud' },
});

const identity = await client.identities.create({
  external_id: 'demo-user',
  name: 'Demo User',
});

const template = await client.templates.create({
  name: 'assistant',
  model: 'auto',
  environment_id: env.id,
});

const session = await client.sessions.create({
  identity_id: identity.id,
  template_id: template.id,
});

await client.sessions.events.send(session.id, {
  events: [{
    type: 'user.message',
    content: [{ type: 'text', text: 'Hello' }],
  }],
});

console.log(session.id);
Use the Managed client to create an Agent and environment, start a session, and send a message:
import { ManagedClient } from 'qca-sdk';

const client = new ManagedClient();

const agent = await client.agents.create({
  name: 'assistant',
  model: 'auto',
});

const env = await client.environments.create({
  name: 'demo',
  config: { type: 'cloud' },
});

const session = await client.sessions.create({
  agent: agent.id,
  environment_id: env.id,
});

await client.sessions.events.send(session.id, {
  events: [{
    type: 'user.message',
    content: [{ type: 'text', text: 'Hello' }],
  }],
});

console.log(session.id);
You can also import the corresponding clients from qca-sdk/forward and qca-sdk/managed. In CommonJS projects, use require('qca-sdk') and place await calls inside an asynchronous function. In subsequent snippets that do not recreate client, it refers to a configured Forward client. Replace IDs such as sess_example with real IDs accessible to the current credentials. See the Managed quickstart and Forward API reference for more integration instructions.

Client configuration

ConfigurationForwardManaged
ClientForwardClientManagedClient
Base URL environment variableQODER_FORWARD_BASE_URLQODER_BASE_URL
Global default URLhttps://api.qoder.com/api/v1/forwardhttps://api.qoder.com/api/v1/cloud
CN URLhttps://api.qoder.com.cn/api/v1/forwardhttps://api.qoder.com.cn/api/v1/cloud
Explicit configuration takes precedence over environment variables. Default PAT mode reads QODER_ACCESS_TOKEN first, then QODER_PAT. The SDK does not automatically load .env files. The Base URL must include the full API prefix and match the region of the account, credentials, and resources.
import { ForwardClient } from 'qca-sdk';

const client = new ForwardClient({
  baseURL: 'https://api.qoder.com.cn/api/v1/forward',
  timeout: 30_000,
  maxRetries: 2,
  defaultHeaders: { 'X-Application': 'my-app' },
});

const result = await client.models.list({ timeout: 5_000 });
console.log(result.data.length);
Time values are in milliseconds. Per-call request options are usually the last method argument. Methods with query or body parameters require those parameters first; do not mix the two.

Reusing configuration

withOptions() creates a new client of the same type, retaining settings that are not overridden. The original client keeps its configuration:
const slowClient = client.withOptions({ timeout: 60_000, maxRetries: 1 });
const result = await slowClient.sessions.list({ limit: 20 });
console.log(result.data.length);
Passing new defaultHeaders, defaultQuery, or middleware replaces the entire corresponding configuration value. Per-request options still take precedence over the derived client defaults.

Authentication

PAT

Pass a personal access token explicitly with pat, or use environment variables. Keep access tokens in a trusted server environment; frontend applications access QCA through their own backend services.

Service Account

After setting QODER_SA_KEY, explicitly create a credential component. It exchanges and caches SATs (service account tokens), refreshing them when subsequent requests require it:
import {
  ForwardClient,
  ManagedClient,
  ServiceAccountCredential,
} from 'qca-sdk';

const credential = ServiceAccountCredential.fromEnv({
  region: 'global',
  scopes: ['qca.access', 'forward.access'],
});

const forward = new ForwardClient({
  credential,
  baseURL: 'https://api.qoder.com/api/v1/forward',
});
const managed = new ManagedClient({
  credential,
  baseURL: 'https://api.qoder.com/api/v1/cloud',
});
Multiple clients can share a credential instance and cache. For the China site, set both region: 'cn' and a CN Base URL. For Managed-only access, you can request the qca.access scope; permissions and workspace bindings must be configured on the server. Ordinary clients do not automatically enable token exchange merely because QODER_SA_KEY exists in the environment. An SA Key cannot serve directly as a business API access token. SA authentication failures do not fall back to PAT. If the application manages SATs itself, use { authMode: 'sa', accessToken: token }. accessToken also accepts a function returning a token or Promise<string>, evaluated before each request attempt. The application must refresh static SATs; do not configure multiple sources such as accessToken, pat, and credential together. See the authentication documentation.

Request and response types

Import request and response types from the subpath for the corresponding mode. Fields retain the API snake_case names:
import type { SessionUpdateParams } from 'qca-sdk/forward';

const omitted: SessionUpdateParams = {};
const explicitNull: SessionUpdateParams = { title: null };
const emptyString: SessionUpdateParams = { title: '' };
JavaScript valueJSON object field behavior
Omitted property or undefined valueOmit the field
nullSend null
false, 0, ''Send as is
Empty array or objectSend [] or {}
Whether a field accepts null and how that value is handled are defined by the corresponding API. Narrow union types using their discriminator or property checks before reading specific fields. Unknown response fields added by the server are preserved, but may not yet appear in the current TypeScript declarations. Check their actual values before declaring the extension types your application needs. TypeScript types alone do not provide runtime data validation.

Streaming responses

streamEvents() returns an asynchronously iterable SSE stream. Read it with for await...of and release the connection in finally:
const stream = await client.sessions.events.streamEvents('sess_example');
try {
  for await (const event of stream) {
    console.log(event.type);
  }
} finally {
  await stream.close();
}
An ordinary stream ends at EOF. To continue subscribing after a recoverable disconnection, explicitly use resumableStream():
const stream = client.sessions.events.resumableStream(
  'sess_example',
  { last_event_id: 'evt_checkpoint' },
  { signal: AbortSignal.timeout(60_000) },
);

try {
  for await (const event of stream) {
    console.log(event.type);
    // Persist this resume position after the application successfully processes the event.
    console.log('cursor:', stream.lastEventID);
  }
} finally {
  await stream.close();
}
Replace evt_checkpoint with a valid resume position; omit last_event_id for an initial subscription. resumableStream() directly returns an asynchronous iterable and does not need await when created. The example uses AbortSignal to limit the whole subscription to 60 seconds; deadline cancellation propagates to the caller as a cancellation error. A resumable stream retains the SSE cursor of the most recently delivered event and reuses it when reconnecting. It does not resend business requests or guarantee exactly-once processing. Multiple deltas for one event may share an ID; do not discard all repeated frames solely by ID. Without a cursor, the stream starts at the current event stream tail; reading history requires a valid historical resume position. HTTP request retry counts are not the reconnection limit for the whole subscription. See the API definitions for event meanings and task completion conditions. Closing a stream or cancelling a local HTTP request ends only the current subscription; it does not automatically stop execution already started in the cloud.

Error handling and Request ID

Use instanceof to distinguish caller cancellation, timeouts, HTTP errors, and other cases:
import {
  APIConnectionTimeoutError,
  APIError,
  APIUserAbortError,
} from 'qca-sdk';

try {
  const result = await client.sessions.retrieve('sess_example');
  console.log(result._request_id);
} catch (error) {
  if (error instanceof APIUserAbortError) {
    console.log('Request canceled');
  } else if (error instanceof APIConnectionTimeoutError) {
    console.log('Request timed out');
  } else if (error instanceof APIError) {
    console.log(error.status, error.code, error.request_id);
  } else {
    throw error;
  }
}
APIError covers HTTP, connection, timeout, and cancellation errors. Without an HTTP response, status, headers, and response may be undefined; do not assume they always exist. Client configuration errors belong to the broader QoderError class. Common HTTP subclasses include BadRequestError (400), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), ConflictError (409), RateLimitError (429), and InternalServerError (5xx). For ordinary JSON object responses, _request_id comes from response headers and is null when absent. It is non-enumerable and does not appear in JSON.stringify() or object spread results. HTTP errors expose the identifier through request_id. Use .withResponse() to read response metadata alongside pagination or streams. Individual stream events and nested objects do not automatically carry this metadata.

Retries and idempotency

By default, requests are retried up to 2 times, for a maximum of 3 attempts. Automatic retries require safe replayability:
RequestRetried by default for
GET, HEAD, or requests with an idempotency keyConnection errors, timeouts, 408, 429, 5xx
Other requests without an idempotency key429
409 responseNo automatic retries
Request body is a non-replayable ReadableStreamNo automatic replay
Within these rules, the SDK handles server retry hints and delays. Some APIs apply stricter rules. Set the default count with the client maxRetries option, or override it in per-call request options:
const result = await client.sessions.retrieve('sess_example', { maxRetries: 0 });
console.log(result.id);
For write requests, pass an idempotency key through idempotencyKey in request options. Reuse the same key for all retries of one logical operation; use a new key for a new operation.

Timeouts and cancellation

The default timeout is 10 minutes. timeout controls how long each underlying fetch waits for a response: timing starts before calling fetch and ends when response headers arrive. It excludes earlier credential acquisition, middleware execution, and subsequent response body reads. SSE or download reads can therefore exceed timeout. To limit the total duration of an operation, pass an AbortSignal:
const result = await client.sessions.retrieve('sess_example', {
  timeout: 10_000,
  signal: AbortSignal.timeout(30_000),
});
console.log(result.id);
The application can also hold an AbortController and call abort() when the user cancels:
const controller = new AbortController();
const pending = client.sessions.retrieve('sess_example', {
  signal: controller.signal,
});

// Call controller.abort() in the application cancellation handler.
const result = await pending;
console.log(result.id);
Signal cancellation ends waiting, retries, and response body reads for the call. Work performed by custom middleware or external credential components should also follow their own cancellation and resource management contracts.

Pagination

List methods support asynchronous iteration, fetching subsequent pages on demand:
for await (const item of client.sessions.list({ limit: 20 })) {
  console.log(item.id);
}
To process one page at a time, read .data and check whether a next page exists:
const page = await client.sessions.list({ limit: 20 });
console.log(page.data.length);

if (page.hasNextPage()) {
  const next = await page.getNextPage();
  console.log(next.data.length);
}
You can also iterate pages with for await (const currentPage of page.iterPages()). getNextPage() throws when there is no next page, rather than returning null; automatic iteration ends normally on the last page. Paginators retain filters and request configuration.

Advanced configuration

Raw responses

Ordinary requests return an APIPromise. Direct await yields parsed data; .withResponse() returns the data, raw response, and Request ID together:
const { data, response, request_id } = await client.sessions
  .retrieve('sess_example')
  .withResponse();

console.log(data.id, response.status, request_id);
.asResponse() returns the raw Response without reading its body. The caller must consume or cancel the body:
const response = await client.sessions.retrieve('sess_example').asResponse();
try {
  console.log(response.status, response.headers.get('x-request-id'));
} finally {
  await response.body?.cancel();
}

Custom fetch and middleware

Use fetch to inject an HTTP implementation. The chosen implementation manages proxies, TLS, and connection pools. Middleware observes or adjusts each API request attempt:
import { ForwardClient } from 'qca-sdk';

const client = new ForwardClient({
  fetch: globalThis.fetch,
  middleware: [
    async (request, next) => {
      const started = Date.now();
      const response = await next(request);
      console.log(request.method, response.status, Date.now() - started);
      return response;
    },
  ],
});
This measures elapsed time until the response arrives, not until SSE consumption finishes. Middleware is part of the API request pipeline, not a global interceptor for every external connection. Use headers and query in request options for additional headers and query parameters. Request fields not yet covered by types can be placed in parameter objects; only fields supported by the server take effect. Prefer existing typed methods for ordinary calls.