TypeScript SDK
Drive HostStack from your own code with @hoststack.dev/sdk: a typed client over the same REST API the dashboard, CLI and MCP server use.
Install
npm install @hoststack.dev/sdkbun add @hoststack.dev/sdkimport { HostStack } from 'npm:@hoststack.dev/sdk';The package ships ES module and CommonJS builds with type definitions, and declares Node.js 18 or later. It has no runtime dependencies: it needs only the global fetch and AbortSignal.timeout, so Bun and Deno work too.
Create a client
Create an API key under Settings → API keys in the dashboard, then pass it to the constructor. The key is sent as a bearer token on every request.
import { HostStack } from '@hoststack.dev/sdk';
const client = new HostStack({ apiKey: process.env.HOSTSTACK_API_KEY! });
// An API key is bound to one team; me() tells you which.
const { team } = await client.me();
const teamId = team!.id;
const { services } = await client.services.list(teamId);apiKey stringrequiredAn
hs_live_…orhs_test_…key. The constructor throwsapiKey is requiredwithout one.baseUrl stringThe API origin. A trailing slash is removed. A non-
httpsURL other thanlocalhostor127.0.0.1logs a warning, because the key would travel in cleartext.Default:
https://hoststack.devtimeoutMs numberPer-request timeout in milliseconds. A request that runs over is aborted and throws a
HostStackErrorwithstatusCode0.0turns the timeout off.Default:
30000maxRetries numberAutomatic retries for failures that are safe to repeat — see Retries and timeouts.
0turns them off.Default:
2userAgent stringThe
User-Agentheader. Set it to name your integration in the API's logs.Default:
hoststack-sdk/<version>
A key's permission is either full access or deploy only. A deploy-only key is refused with a ForbiddenError on every call that needs full access — bulk env var writes and deleting a volume among them.
Team IDs
Every resource method takes the team as its first argument. The only calls that don't are client.me() and client.teams.list(), which are how you find it:
client.me()returns{ user, team, apiKey }. With an API key,userisnullandteamis the team the key belongs to.client.teams.list()returns the teams the caller can reach — for an API key, that one team.hoststack whoamiprints it as Team ID, and the MCP server'sget_metool returns it.
The team argument takes the numeric id or the team's team_… public id.
IDs
Most id arguments are typed IdInput: a number, a numeric string, or a public id with the right prefix. The SDK turns a public id into the numeric id the API uses by reading the matching list once, then caches the answer for the life of the client.
| Resource | Public id prefix |
|---|---|
| team | team_ |
| project | prj_ |
| service | svc_ |
| deploy | dpl_ |
| database | db_ |
| domain | dom_ |
| volume | vol_ |
| environment | env_ |
| cron execution | cjob_ |
| dev task | task_ |
A public id with the wrong prefix throws a HostStackError with status 400; one that matches nothing throws NotFoundError.
Resources
The client has 18 resources. Each method returns a promise of the JSON the API answers with, usually wrapped in a named key — { service }, { databases } — and undefined for calls that answer 204 No Content.
| Property | Methods |
|---|---|
| client.projects | list, get, create, update, delete |
| client.services | list, get, create, update, delete, resize, suspend, resume, getMetrics, getMetricsHistory, getConfig, updateConfig, getRuntimeLogs, streamLogs, listDevEnvironments, createDevEnvironment, spinUpDevEnvironment, tearDownDevEnvironment |
| client.deploys | list, get, trigger, cancel, rollback, promote, getLogs |
| client.databases | list, get, create, update, delete, suspend, resume, restart, getCredentials, resetPassword, query, upgradeToHa, upgradeVersion, getCluster, listRestorePoints |
| client.domains | list, add, update, remove, verify |
| client.envVars | list, create, update, delete, bulkSet, importFile |
| client.environments | list, get, create, update, delete |
| client.cron | list, get, trigger |
| client.volumes | list, create, update, delete, listRestorePoints, restore |
| client.devTasks | list, counts, get, create, update, delete |
| client.machines | list, get |
| client.notifications | listChannels, createChannel, updateChannel, deleteChannel, testChannel |
| client.analytics | listSites, createSite, updateSite, siteStatus, rotateKey, deleteSite, verifyDomain, getDomainProof, summary, overview, realtime, eventMetadata, myConnection, searchConsole, searchConsoleStatus, connectSearchConsole, disconnectSearchConsole, searchConsoleAccess, connectAllSearchConsole, bing, bingCredential, saveBingCredential, removeBingCredential, bingSites, bingStatus, connectBing, connectAllBing, disconnectBing |
| client.errors | listIssues, getIssue, listOccurrences, updateIssue, fixInDevBox, listIngestKeys, createIngestKey, deleteIngestKey |
| client.uptime | get, upsert, remove, getForSite, upsertForSite, removeForSite |
| client.teams | list |
| client.dns | listZones, createZone, deleteZone, checkDelegation, listRecords, createRecord, updateRecord, deleteRecord, resyncRecord |
| client.serviceResourceLinks | listManagedResources, list, create, updateAlias, delete |
client.request(method, path, body?) is public too: it sends an authenticated request to any API path with the same retries and typed errors, for an endpoint the resources don't wrap yet. See the API reference.
Projects, environments and teams
| Method | What it does |
|---|---|
| projects.list(teamId) | Returns { projects }. |
| projects.get(teamId, projectId) | Returns { project }. |
| projects.create(teamId, { name, description?, region? }) | Returns { project }. |
| projects.update(teamId, projectId, data) | Returns { project }. |
| projects.delete(teamId, projectId) | Deletes the project. |
| environments.list(teamId, projectId) | Returns { environments }. |
| environments.get(teamId, projectId, envId) | Returns { environment }. |
| environments.create(teamId, projectId, { name, type, isProtected? }) | type is production, staging, development or preview. |
| environments.update(teamId, projectId, envId, data) | Changes the name, the default flag or the protected flag. |
| environments.delete(teamId, projectId, envId) | Refused while the environment still has services or databases, and for the project's default environment. |
| teams.list() | Returns { teams }. Takes no team id. |
Services
| Method | What it does |
|---|---|
| services.list(teamId, filters?) | Filters: projectId, environmentId, status, type. Dev boxes are left out unless you pass devEnvironment: true. |
| services.get(teamId, serviceId) | Returns { service }. |
| services.create(teamId, data) | Returns { service, deployId, linkErrors }. A service with a repo starts its first deploy. |
| services.update(teamId, serviceId, data) | Build and source settings: branch, auto-deploy, install/build/start commands, health check path, runtime, plan. |
| services.delete(teamId, serviceId) | Deletes the service. |
| services.resize(teamId, serviceId, plan) | Moves the service to another size tier. Memory and CPU apply to the running container; disk grows on the next recreate. |
| services.suspend(teamId, serviceId) | Stops the service. |
| services.resume(teamId, serviceId) | Starts a suspended service. |
| services.getMetrics(teamId, serviceId) | The latest sample. metrics is null until the first one lands. |
| services.getMetricsHistory(teamId, serviceId, { from?, to? }) | A time series; ISO-8601 bounds, the trailing hour when both are left out. |
| services.getConfig(teamId, serviceId) | Returns { config }: resources, port, health checks, scaling. |
| services.updateConfig(teamId, serviceId, data) | Changes runtime configuration such as memoryMb, cpuShares, diskSizeGb, port and health checks. |
| services.getRuntimeLogs(teamId, serviceId, options?) | since and until take ISO-8601 or a relative offset like -5m. countOnly: true returns { count } instead of lines. |
| services.streamLogs(teamId, serviceId, options?) | An async iterator of runtime log lines. See below. |
| services.listDevEnvironments(teamId) | Dev boxes, each with its companion databases. |
| services.createDevEnvironment(teamId, data) | A standalone dev box. Returns { service, devUrl, deployId }. |
| services.spinUpDevEnvironment(teamId, serviceId, options?) | A dev box cloned from a service, with its env vars and a clone of its linked database (includeDatabaseClone). |
| services.tearDownDevEnvironment(teamId, serviceId) | Deletes a dev box with its cloned database and /workspace volume. |
streamLogs polls the logs endpoint — 100 lines first, then every 2 seconds — and yields each new entry. Stop it with an AbortSignal; the loop ends without throwing.
const ac = new AbortController();
setTimeout(() => ac.abort(), 60_000); // stop after a minute
for await (const entry of client.services.streamLogs(teamId, 'svc_...', {
signal: ac.signal,
stream: 'stderr', // optional
pollInterval: 2000, // default
initialLines: 100, // default
})) {
console.log(entry.timestamp, entry.level, entry.message);
}Deploys
| Method | What it does |
|---|---|
| deploys.list(teamId, serviceId, { page?, perPage? }) | Paginated: { data, page, perPage, total, totalPages }. 25 per page by default, 100 at most. |
| deploys.get(teamId, serviceId, deployId) | Returns { deploy }. |
| deploys.trigger(teamId, serviceId, { commitHash?, branch? }?) | Starts a deploy. Returns { deploy }. |
| deploys.cancel(teamId, serviceId, deployId) | Cancels a deploy in progress. |
| deploys.rollback(teamId, serviceId, deployId) | Goes back to an earlier deploy. |
| deploys.promote(teamId, serviceId, deployId, targetEnvironmentId) | Runs the same built image in another environment, without a rebuild. Creates the sibling service there if there is none. |
| deploys.getLogs(teamId, serviceId, deployId, options?) | Build logs: { logs, nextAfterId }. Pass afterId for the next page; 500 lines by default, 5000 at most. |
A deploy's status is one of pending, building, build_failed, deploying, live, failed, cancelled, deactivated or superseded.
const { deploy } = await client.deploys.trigger(teamId, 'svc_...');
const done = new Set(['live', 'failed', 'build_failed', 'cancelled']);
let status = deploy.status;
while (!done.has(status)) {
await new Promise((r) => setTimeout(r, 5_000));
({ deploy: { status } } = await client.deploys.get(teamId, 'svc_...', deploy.id));
}Environment variables
| Method | What it does |
|---|---|
| envVars.list(teamId, serviceId) | Returns { envVars }. Secret values come back masked. |
| envVars.create(teamId, serviceId, { key, value, target?, isSecret? }) | target is build, runtime or both (the default). |
| envVars.update(teamId, serviceId, envVarId, { value?, target?, isSecret? }) | envVarId is numeric — env vars have no public id. Read it from list. |
| envVars.delete(teamId, serviceId, envVarId) | Deletes one variable. |
| envVars.bulkSet(teamId, serviceId, { vars }) | Replaces every variable on the service with vars — up to 1000. Anything not in the list is deleted, and an empty list clears them all. |
| envVars.importFile(teamId, serviceId, { content, dryRun?, mode?, target?, isSecret? }) | Imports a .env file, parsed strictly on the server. A dry run unless dryRun: false; mode is merge (default) or replace; values default to runtime-only secrets. |
Keys match ^[A-Za-z_][A-Za-z0-9_]*$, up to 256 characters, and are case-sensitive. Values can be up to 32,768 characters. Changes reach the container on the next deploy.
await client.envVars.create(teamId, 'svc_...', {
key: 'STRIPE_SECRET_KEY',
value: 'sk_live_...',
isSecret: true,
});
// Everything not listed here is removed from the service.
await client.envVars.bulkSet(teamId, 'svc_...', {
vars: [
{ key: 'NODE_ENV', value: 'production' },
{ key: 'API_SECRET', value: '...', isSecret: true },
],
});Databases
| Method | What it does |
|---|---|
| databases.list(teamId, projectId?) | Returns { databases }, for one project or the whole team. |
| databases.get(teamId, databaseId) | Returns { database }. |
| databases.create(teamId, data) | name, engine and a numeric projectId are required. Optional: version, plan, environmentId, machineId, postgis, pgvector. |
| databases.update(teamId, databaseId, data) | Returns { database }. |
| databases.delete(teamId, databaseId) | Deletes the database. |
| databases.suspend(teamId, databaseId) | Stops the container and keeps the data. |
| databases.resume(teamId, databaseId) | Starts a suspended database. |
| databases.restart(teamId, databaseId) | Restarts the container in place: a few seconds of downtime, same connection URL. The database must be available. |
| databases.getCredentials(teamId, databaseId) | Returns { credentials }: host, port, username, password, databaseName, connectionUrl. |
| databases.resetPassword(teamId, databaseId) | Sets a new password. |
| databases.query(teamId, databaseId, sql) | One read-only statement against Postgres: 30-second timeout, 1000 rows at most, audit-logged. Returns { columns, rows, rowCount, truncated, durationMs }. |
| databases.upgradeVersion(teamId, databaseId, version) | Upgrades Postgres or Redis in place, keeping the connection URL. Asynchronous: poll get until version matches. |
| databases.upgradeToHa(teamId, databaseId) | Migrates a standalone Postgres to a three-node cluster. Asynchronous, and refused with 403 unless HA is enabled for the team. |
| databases.getCluster(teamId, databaseId) | Cluster members and failover history. 400 for a standalone database. |
| databases.listRestorePoints(teamId, databaseId) | The off-site backups that exist, newest first. |
| Engine | Versions | Default |
|---|---|---|
| postgres | 18, 17, 16, 15 | 18 |
| redis | 8, 7, 6 | 8 |
| mysql | 8.4, 8.0, 5.7 | 8.4 |
| mariadb | 11.4, 10.11 | 11.4 |
| mongodb | 8, 7, 6 | 8 |
const { database } = await client.databases.create(teamId, {
name: 'app-db',
engine: 'postgres',
projectId: project.id, // numeric, not the prj_… public id
});
const { credentials } = await client.databases.getCredentials(teamId, database.publicId);
console.log(credentials.connectionUrl);Resource links
A database is not reachable from a service until you link it. A link injects the resource's connection details as environment variables, prefixed with its alias — a database linked as APP_DB gives APP_DB_URL, APP_DB_HOST, APP_DB_PORT, APP_DB_DATABASE, APP_DB_USERNAME and APP_DB_PASSWORD. The first linked database of each kind also sets DATABASE_URL (Postgres, MySQL, MariaDB), REDIS_URL or MONGO_URL, unless you set that variable yourself.
| Method | What it does |
|---|---|
| serviceResourceLinks.listManagedResources(teamId) | Every linkable resource the team owns — databases, object storage, queues, search, email domains — with its numeric id. |
| serviceResourceLinks.list(teamId, serviceId) | Returns { links }. |
| serviceResourceLinks.create(teamId, serviceId, { resourceType, resourceId, alias }) | resourceId is numeric. alias is uppercase letters, digits and underscores, starting with a letter. |
| serviceResourceLinks.updateAlias(teamId, serviceId, linkId, alias) | Renames the injected variables on the next deploy. |
| serviceResourceLinks.delete(teamId, serviceId, linkId) | Removes the link. The resource itself is kept. |
await client.serviceResourceLinks.create(teamId, 'svc_...', {
resourceType: 'database',
resourceId: database.id, // numeric id, not db_…
alias: 'APP_DB',
});
// A link takes effect on the NEXT deploy — nothing changes until then.
await client.deploys.trigger(teamId, 'svc_...');Domains and DNS
| Method | What it does |
|---|---|
| domains.list(teamId) | Returns { domains }. |
| domains.add(teamId, data) | Attaches a custom domain. The result can carry a delegationWarning: HostStack hosts the zone, but the registry still points the domain elsewhere, so verification cannot pass yet. |
| domains.update(teamId, domainId, data) | Returns { domain }. |
| domains.remove(teamId, domainId) | Removes the domain. |
| domains.verify(teamId, domainId) | Checks DNS and returns the domain as it stands after — read its status. |
| dns.listZones(teamId) | Returns { zones }. |
| dns.createZone(teamId, domain) | Creates a hosted zone. It serves nobody until the registrar points the domain at ns1.hoststack.dev and ns2.hoststack.dev. |
| dns.deleteZone(teamId, zoneId) | Deletes the zone and its records. |
| dns.checkDelegation(teamId, zoneId) | Reads the registry now. listZones carries the last reading, refreshed hourly. |
| dns.listRecords(teamId, zoneId) | Returns { records }. |
| dns.createRecord(teamId, zoneId, data) | Creates a record. |
| dns.updateRecord(teamId, zoneId, recordId, data) | Replaces a record — every field must be sent. |
| dns.deleteRecord(teamId, zoneId, recordId) | Deletes a record. |
| dns.resyncRecord(teamId, zoneId, recordId) | Pushes a record left at failed to the provider again. |
Volumes
| Method | What it does |
|---|---|
| volumes.list(teamId, serviceId) | Returns { volumes }. |
| volumes.create(teamId, serviceId, { name, mountPath, sizeGb? }) | Attaches a volume: 10 to 10,240 GB, 10 by default. |
| volumes.update(teamId, serviceId, volumeId, data) | Changes mountPath or sizeGb. Both take effect on the next deploy. |
| volumes.delete(teamId, serviceId, volumeId) | Detaches the volume and destroys the disk. |
| volumes.listRestorePoints(teamId, serviceId, volumeId) | The backups that exist, newest first. |
| volumes.restore(teamId, serviceId, volumeId, { backupId } | { s3Url }) | Unpacks a backup over the volume. |
Cron jobs
| Method | What it does |
|---|---|
| cron.list(teamId, serviceId, { limit? }?) | Recent runs of a cron job service: { executions }. |
| cron.get(teamId, serviceId, executionId) | Returns { execution }. |
| cron.trigger(teamId, serviceId) | Runs the job now. |
Machines
Your own hardware, once enrolled. The SDK can only read machines: enrolling one prints an installer that has to be run on the machine itself, so it is done with hoststack machines add or in the dashboard.
| Method | What it does |
|---|---|
| machines.list(teamId) | Returns { machines }. |
| machines.get(teamId, machineId) | Returns { machine, running }. running is what stops with the machine — read it before switching one off. |
const { machines } = await client.machines.list(teamId);
const desktop = machines.find((m) => m.name === 'desktop');
if (!desktop) throw new Error('enrol it first: hoststack machines add desktop');
await client.services.create(teamId, {
name: 'api',
type: 'web_service',
projectId: project.id,
githubRepo: 'acme/my-app',
machineId: desktop.id, // leave it out to run on HostStack compute
});machineId is fixed when a service, database or dev box is created. A database on your own machine can only be linked to services on that same machine.
Dev tasks
| Method | What it does |
|---|---|
| devTasks.list(teamId, projectId) | A project's backlog: { tasks, automodeEnabled }. |
| devTasks.counts(teamId) | Open tasks per dev box, for the whole team. |
| devTasks.get(teamId, taskId) | Returns { task }. |
| devTasks.create(teamId, { projectId, title, body?, serviceId? }) | Files a task. serviceId pins it to a dev box. |
| devTasks.update(teamId, taskId, data) | Edits it, or sets status to done or idea. |
| devTasks.delete(teamId, taskId) | Deletes it. Setting done keeps the history instead. |
Error tracking
| Method | What it does |
|---|---|
| errors.listIssues(teamId, options?) | Grouped issues, unresolved unless you pass status. Also filters by serviceId and q, and sorts by last_seen, first_seen or count. |
| errors.getIssue(teamId, issueId) | Returns { issue }. |
| errors.listOccurrences(teamId, issueId, limit?) | Stored samples, newest first; 20 by default. Samples age out after 30 days. |
| errors.updateIssue(teamId, issueId, status) | resolved, ignored or unresolved. |
| errors.fixInDevBox(teamId, issueId, { serviceId? }?) | Writes a task for a coding agent in the project’s dev box. It does not start the agent. |
| errors.listIngestKeys(teamId, serviceId) | The service’s ingest keys, without the secrets. |
| errors.createIngestKey(teamId, serviceId, name?) | Mints a write-only ingest key. The key is in this response and nowhere else. |
| errors.deleteIngestKey(teamId, serviceId, keyId) | Revokes a key at once. |
Uptime checks
| Method | What it does |
|---|---|
| uptime.get(teamId, serviceId) | Returns { check }, null when there is none. |
| uptime.upsert(teamId, serviceId, input?) | Creates or replaces the check. Fields you leave out go back to their defaults, not to what is stored. |
| uptime.remove(teamId, serviceId) | Stops checking the service. |
| uptime.getForSite(teamId, siteId) | The check on an analytics site. |
| uptime.upsertForSite(teamId, siteId, input?) | Checks a site by hostname. Its domain must be verified first. |
| uptime.removeForSite(teamId, siteId) | Stops checking the site. |
The input fields are enabled, path, method (GET or HEAD), expectedStatus, timeoutMs, intervalSeconds (30 to 3600) and failureThreshold.
Analytics
| Method | What it does |
|---|---|
| analytics.listSites(teamId) | Returns { sites }. |
| analytics.createSite(teamId, { domain, name?, serviceId?, retentionDays? }) | Starts tracking a domain, hosted on HostStack or anywhere else. |
| analytics.updateSite(teamId, siteId, data) | Changes the name, linked service, retention or allowed origins. |
| analytics.siteStatus(teamId, siteId) | Whether events arrive, the last one, what was refused and why. |
| analytics.rotateKey(teamId, siteId) | Issues a new site key. The old one keeps working for 30 days. |
| analytics.deleteSite(teamId, siteId) | Deletes the site and every event recorded for it. |
| analytics.getDomainProof(teamId, siteId) | The TXT record that proves you own the domain. |
| analytics.verifyDomain(teamId, siteId) | Reads DNS for that record now. |
| analytics.summary(teamId, { siteIds?, range? }?) | One row per site, side by side. |
| analytics.overview(teamId, options?) | The full breakdown for one site, several or all. |
| analytics.realtime(teamId, { siteIds? }?) | Visitors in the last five minutes. |
| analytics.eventMetadata(teamId, eventType, options?) | The top metadata values recorded with a custom event. |
| analytics.searchConsole(teamId, options?) | Google Search Console for linked sites: queries, pages with the organic visits they brought, index state and impression cliffs. |
| analytics.connectSearchConsole(teamId, siteId, { property? }) | Ask the site’s owner to link it: returns a dashboard link they finish with their Google account. The site reads pending until they do. |
| analytics.searchConsoleStatus(teamId, siteId) | Where the link stands: not_connected, pending, expired, active (with backfilling), or why it is broken. |
| analytics.disconnectSearchConsole(teamId, siteId) | Unlink a site, delete its search data and revoke the Google grant. |
| analytics.connectAllSearchConsole(teamId, { siteIds? }) | Link every site to the Search Console property that covers it, with the Google access a member lent the team’s agents. Safe to repeat. |
| analytics.searchConsoleAccess(teamId) | Whether a member has let the team’s agents link sites with their Search Console access, and who. |
| analytics.bing(teamId, options?) | Bing Webmaster Tools for linked sites: daily clicks and impressions, weekly queries and pages, crawl figures and sitemaps. |
| analytics.saveBingCredential(teamId, apiKey) | Save the team’s Bing API key (admin or full-access key). Bing checks it first; returns the sites it can read. |
| analytics.bingCredential(teamId) | Whether the team has a Bing key saved. Only its last four characters are ever returned. |
| analytics.removeBingCredential(teamId) | Forget the team’s Bing key. Linked sites keep their history and pause until a key is saved again. |
| analytics.bingSites(teamId) | Every site the team’s Bing key can read, with the analytics site each matches. |
| analytics.connectAllBing(teamId, { addMissing? }) | Link every site to Bing. Sites Bing lacks are added to the key’s account and verified through DNS where HostStack hosts it (addMissing, default on). Safe to repeat. |
| analytics.connectBing(teamId, siteId, { siteUrl? }) | Link one site, to the Bing site on the same host unless one is named. The first pull starts at once. |
| analytics.bingStatus(teamId, siteId) | Where a site’s Bing link stands: not_connected, active (with backfilling), needs_key, needs_site or error. |
| analytics.disconnectBing(teamId, siteId) | Unlink a site from Bing and delete its Bing data. |
| analytics.myConnection(teamId) | Your address as ingest sees it, and the rule that excludes it from a site’s numbers. |
range is 24h, 7d, 30d, 90d or 12mo.
Notifications
| Method | What it does |
|---|---|
| notifications.listChannels(teamId) | Returns { channels }, with webhook URLs masked. |
| notifications.createChannel(teamId, { type, name, webhookUrl, events }) | type is slack, discord or email (then webhookUrl is the address). An empty events list receives nothing. |
| notifications.updateChannel(teamId, channelId, { name?, active?, events? }) | The type and URL cannot change — create a new channel instead. |
| notifications.deleteChannel(teamId, channelId) | Deletes the channel. |
| notifications.testChannel(teamId, channelId) | Sends a test event. Returns { success, error? }. |
Retries and timeouts
Retries depend on whether repeating the request is safe:
- GET requests are retried on
429, any5xx, a network error and a timeout. - POST, PUT, PATCH and DELETE are retried only on
429, which means the request was refused before it ran. After a5xxor a dropped connection the change may already have happened, so retrying could deploy twice.
A retry waits for the Retry-After header when there is one (up to 60 seconds), and otherwise backs off from 250 ms, doubling each time. With the default maxRetries: 2, a call is tried at most three times before the error reaches you.
Error handling
Every failed request throws a HostStackError or one of its subclasses. statusCode is the HTTP status (0 for a timeout or network error), message is the API's error text, and body is the parsed response, with field-level validation details when there are any.
| Class | Status |
|---|---|
| AuthenticationError | 401 — the key is missing, wrong or revoked |
| ForbiddenError | 403 — the key lacks the permission, e.g. a deploy-only key |
| NotFoundError | 404, or a public id that matches nothing |
| ConflictError | 409 — the resource’s state prevents the operation |
| RateLimitError | 429, after the retries ran out. retryAfter holds the Retry-After seconds, if sent. |
| HostStackError | Everything else, and the base class of the above |
import {
AuthenticationError,
HostStackError,
NotFoundError,
RateLimitError,
} from '@hoststack.dev/sdk';
try {
await client.services.get(teamId, 'svc_...');
} catch (err) {
if (err instanceof NotFoundError) {
console.log('No such service');
} else if (err instanceof RateLimitError) {
// The SDK has already retried; wait as long as the API asked.
await new Promise((r) => setTimeout(r, (err.retryAfter ?? 5) * 1000));
} else if (err instanceof AuthenticationError) {
console.log('API key rejected — create a new one under Settings → API keys');
} else if (err instanceof HostStackError) {
console.log(err.statusCode, err.message);
} else {
throw err;
}
}Next steps
Was this page helpful?