The Qoren TypeScript SDK
Use @qoren/sdk to drive Qoren from TypeScript: authenticate with a token, manage environments, agents, secrets and webhooks, and await long-running jobs.
On this page
@qoren/sdk is the TypeScript client for the Qoren API. It is what the Qoren command line is built on, so anything the CLI does you can do from your own code: create environments, deploy and message agents, manage vault secrets and webhook triggers, and read usage.
Before you start#
- Node.js 20 or newer (the SDK uses the built-in
fetch). - A plan that includes API access: Ultimate, Business or Enterprise. The SDK authenticates with an access token, and on any other plan every call is refused (see handle errors). The web console works on every plan.
- A personal access token. Create one under Settings, CLI tokens, or run
qoren login. See create and revoke access tokens.
Install and connect#
npm install @qoren/sdkimport { Qoren } from "@qoren/sdk";
const qoren = new Qoren({ token: process.env.QOREN_TOKEN });
const environments = await qoren.environments.list();
console.log(environments.map((e) => e.name));The constructor takes:
| Option | Meaning |
|---|---|
token | Your personal access token (qrn_...) |
baseUrl | The Qoren server. Default https://qoren.sh |
timeoutMs | Per-request ceiling. Default 15 minutes, because some calls run for minutes |
userAgent | Sent as User-Agent, so you can tell your client apart in logs |
fetch | Your own fetch implementation, for tests or instrumentation |
Every request goes to Qoren's API gateway, the same one the console and the CLI use. It checks the token, applies your plan's limits and records the action, so the SDK can do exactly what your account can do and nothing more.
Know the vocabulary#
The SDK uses the product's words. Where the API path differs, the method hides it.
| SDK | API path | What it is |
|---|---|---|
environments | machines | The private computers agents run on |
templates | templates | What an agent is built from |
clients | customers | Agency clients you run environments for |
account.clients() | clients | Your organization's own record, whose slug creating an environment needs |
Work with each resource#
Account#
const usage = await qoren.account.usage(); // credits used and left, blocked, budgetPaused
const options = await qoren.account.options(); // sizes, regions, models and their defaults
const spend = await qoren.account.spending(30); // model spend per environment, last 30 days
const costs = await qoren.account.costs({ from: new Date("2026-09-01") }); // per clientusage.blocked means the account is out of credits; usage.budgetPaused means the monthly budget you set was reached. Both stop agents, and they have different fixes: top up for the first, raise or clear the budget for the second. See spend controls.
Other methods: fleetSummary(), clients() and byok().
Environments#
const slug = await qoren.resolveOrgSlug();
const { jobId, machineId } = await qoren.environments.create({
clientSlug: slug,
name: "production",
size: "s-2vcpu-2gb-90gb-intel",
});
await qoren.jobs.await(jobId!);size must be one of the four plan size slugs (listed in the Qoren command line); a size name such as "light" is refused with a 409, "That environment size isn't available on your plan.", as is a slug above your plan. account.options() lists every size the platform knows, not only the ones your plan accepts. Your plan sets the largest size you may use; see environment sizes. Other methods: list(), get(id), rename(id, name), resize(id, size) (larger sizes only; it restarts the environment), destroy(id), vitals(id), setCollaboration(id, enabled), teammateMessages(id) and assignClient(id, clientId).
Agents#
const options = await qoren.account.options();
const { jobId, safetyWarnings } = await qoren.agents.create({
machineId: "env_abc123",
slug: "inbox-triage",
name: "Inbox triage",
runtime: "hermes",
model: options.defaultModel,
presetName: "inbox-triage",
clientSecretNames: ["GMAIL_TOKEN"],
});
await qoren.jobs.await(jobId);
const turn = await qoren.agents.message("agt_def456", "What came in today?");
const job = await qoren.jobs.await(turn.jobId);
console.log(job.result); // { exitCode, stdOut, stdErr }runtime is hermes, openclaw or codex. slug is the agent's permanent identity and cannot change later; rename changes only the display name. Only secret names cross the wire; values are filled in on the server from your vault.
A message runs one turn and can take minutes, so message returns a job and the reply is the job's result. Pass the session id from a reply as the third argument to continue that conversation.
Other methods, grouped:
| Area | Methods |
|---|---|
| Lifecycle | list(environmentId?), get, destroy, rename, configure, reprovision, move, snapshots, snapshot, restore, listDeleted, restoreDeleted |
| Running | exec, logs, healthcheck, diagnostics, activity, telemetry, usage, doctorRuns, runDoctor |
| Workspace | workspace, readFile, readFiles, writeFile, renameFile, deleteFile, fileUrl |
| Public links | createFileLink, listFileLinks, revokeFileLink |
| Scheduled tasks | listScheduledTasks, listScheduledTaskRuns, createScheduledTask, replaceScheduledTask, deleteScheduledTask |
| Keys and sign-in | envKeys, deviceAuth, startDeviceLogin, deviceLogout |
| Teammates | teammateMessages |
| Terminal | openTerminal, terminalIo, terminalTicket, closeTerminal |
exec runs a shell command as the agent's own user and returns { exitCode, stdOut, stdErr } directly.
Clients#
For agencies with clients turned on (see clients). Without the feature, these answer 403 with "Clients are not enabled for this account."
const client = await qoren.clients.create({ name: "Acme Dental", contactEmail: "ops@acme.example" });
await qoren.environments.assignClient("env_abc123", client.id);Other methods: list({ includeArchived }), update(id, input) and archive(id). Archiving is refused while environments are still assigned.
Jobs#
list(limit?), get(id), cancel(id) and await(id, onProgress?, signal?). The next section shows how to follow one.
Secrets#
The vault, scoped by your organization slug. Values are write-only: list returns names and details, never values.
const slug = await qoren.resolveOrgSlug();
await qoren.secrets.set(slug, { name: "STRIPE_KEY", value: process.env.STRIPE_KEY! });
const who = await qoren.secrets.usedBy(slug, "STRIPE_KEY");Other methods: list(slug), remove(slug, name), and reveal(slug, name), which returns one value and is recorded in your account log every time. See secrets.
Templates and skills#
const templates = await qoren.templates.list(); // the gallery plus your own
const full = await qoren.templates.get("inbox-triage");Other methods: templates.save(body) (runs the spend and security review first) and templates.remove(slug); skills.list(), skills.get(slug) and skills.setForAgent(agentId, slugs).
Webhooks#
const { url, secret } = await qoren.webhooks.create("agt_def456", {
name: "Bookings",
source: "cal",
events: ["BOOKING_CREATED"],
instructions: "Add the attendee to the CRM and brief me.",
});The URL and secret come back once, from create and rotate; store them. Other methods: sources(), list(agentId), get(id), update(id, input), rotate(id), remove(id), test(id), deliveries(id, limit?), delivery(id, deliveryId) and replay(id, deliveryId). See webhooks.
Approvals#
const waiting = await qoren.approvals.list(); // status "pending" by default
for (const request of waiting) {
console.log(request.agentName, request.source, request.displayCommand, request.expiresAt);
}
await qoren.approvals.decide(waiting[0].agentId, [
{ approvalId: waiting[0].id, approve: false, note: "Not this week." },
]);list({ status, limit }) takes pending, decided or all. decide(agentId, decisions) settles some of one agent's requests and returns the ids of the jobs it started. See approve what your agents ask to do.
Follow a job#
Anything expensive returns { jobId } straight away and does its work in steps. qoren.jobs.await polls until the job finishes, resolves with the finished job, and throws the job's own error if it failed or was cancelled:
const job = await qoren.jobs.await(jobId, (snapshot) => {
if (!snapshot) return console.log("reconnecting…");
const done = snapshot.steps.filter((s) => s.status === "Succeeded").length;
console.log(`${done}/${snapshot.steps.length} ${snapshot.status}`);
});It polls quickly at first and slows down for long jobs. If the API is briefly unavailable, it keeps waiting and calls your callback with null instead of failing. Pass an AbortSignal as the third argument to stop waiting.
Handle errors#
Every failed call throws a QorenError with the server's message, the HTTP status, and the parsed body.
| Property | True or set when |
|---|---|
isAuthError | 401 or 403: the token is missing, revoked, or not allowed this call. False for isApiAccessRequired |
isApiAccessRequired | 403 with code api_access_required: the token is fine, but the organization's plan does not include API access |
isPaymentRequired | 402: this needs an active plan |
retryable | A brief outage; trying again is safe |
code | The body carries a machine-readable code |
regionUnavailable | An environment create was refused because the pinned region cannot run the size |
sizeUnavailable | A resize was refused because the environment's region cannot run the size |
A create that names a region pins it. If that region cannot run the size, you get a 409, and regionUnavailable tells you the ways out:
import { Qoren, QorenError } from "@qoren/sdk";
try {
await qoren.environments.create({ clientSlug, name, size, region: "nyc1" });
} catch (e) {
const offer = e instanceof QorenError ? e.regionUnavailable : null;
if (!offer) throw e;
// offer.suggestedRegion: the closest region that has the size, or null
// offer.availableSizes: sizes nyc1 does have, each { slug, key, label }
if (offer.suggestedRegion) {
await qoren.environments.create({ clientSlug, name, size, region: "nyc1", autoRegion: true });
}
}Leave region out and the platform picks a region that can run the size, with no refusal. resize answers the same way under sizeUnavailable, without a region option, because an environment cannot move; pick one of its availableSizes instead.
When the pre-deploy review refuses an agent or a template, the error is a 422 and body.safetyFindings lists each finding with a suggestion.
When the organization's plan does not include API access, every call is refused the same way, whatever the path. The message names the plans that include it, and the body carries the details:
try {
await qoren.environments.list();
} catch (e) {
if (e instanceof QorenError && e.isApiAccessRequired) {
// e.body: { code: "api_access_required", plan: "pro",
// plans: ["Ultimate", "Business", "Enterprise"],
// upgradeUrl: "https://qoren.sh/pricing" }
console.error(e.message);
}
throw e;
}Signing in again does not help; upgrading does. A plan change reaches a token already in use within about 30 seconds.
Call an endpoint the SDK does not wrap#
qoren.raw reaches any endpoint with the same token and rules:
const summary = await qoren.raw("fleetSummary");
const created = await qoren.raw("agents", { method: "POST", body: { /* ... */ } });
const recent = await qoren.raw("machines", { query: { limit: 5 } });The path is relative to the API root. Operator-only endpoints answer 403 to every customer account.
Frequently asked questions#
Can I use the SDK in a browser?
It runs anywhere fetch exists, but never ship a personal access token to a browser: anyone who can read a token can act as you. Call the SDK from your own server instead.
Which package name and repository is it?
The npm package is @qoren/sdk, and its source is at github.com/qoren-sh/sdk under the Apache 2.0 license.
Why does creating an agent return before the agent exists?
Deploying is a job that installs and configures the agent on its environment in several steps. create returns the job id straight away; await it with qoren.jobs.await.
Which plans can use the SDK?
Ultimate, Business and Enterprise, including a free trial of one of them. On Starter, Pro or with no plan, calls throw a QorenError with isApiAccessRequired set. Compare plans on the pricing page.
Does the SDK retry failed calls?
Only while awaiting a job, where brief outages are ridden out. A single call that fails throws; check retryable to decide whether to try again.