Perstack API Client
npm install @perstack/api-clientOfficial TypeScript API client for the Perstack platform.
``bash`
npm install @perstack/api-client
`typescript
import { createApiClient } from "@perstack/api-client"
const client = createApiClient({
apiKey: "your-api-key",
})
// List applications
const result = await client.applications.list()
if (result.ok) {
console.log(result.data.data.applications)
} else {
console.error(result.error.message)
}
`
The createApiClient function accepts a configuration object:
`typescript`
interface ApiClientConfig {
apiKey: string // Required: Your Perstack API key
baseUrl?: string // Optional: API base URL (default: "https://api.perstack.ai")
timeout?: number // Optional: Request timeout in milliseconds (default: 30000)
}
The client provides access to four main API modules:
- client.applications - Application managementclient.env
- - Environment configuration (secrets and variables)client.jobs
- - Job execution and monitoringclient.experts
- - Expert definitions and versioning
All API methods return a discriminated union type for type-safe error handling:
`typescript
type ApiResult
interface ApiError {
code: number // HTTP status code (0 for network/validation errors)
message: string // Error message
reason?: unknown // Additional error details
aborted?: boolean // True if request was aborted
}
`
All API methods accept an optional RequestOptions object:
`typescript`
interface RequestOptions {
signal?: AbortSignal // AbortController signal for cancellation
}
For streaming endpoints, an extended StreamRequestOptions is available:
`typescript`
interface StreamRequestOptions extends RequestOptions {
streamIdleTimeout?: number // Idle timeout in ms between chunks (default: client timeout)
}
---
Manage applications within your organization.
#### client.applications.list(params?, options?)
List all applications with optional filtering and pagination.
`typescript`
const result = await client.applications.list({
name: "my-app", // Filter by name
sort: "createdAt", // Sort by: "name" | "createdAt" | "updatedAt"
order: "desc", // Order: "asc" | "desc"
take: 10, // Number of results
skip: 0, // Offset for pagination
})
#### client.applications.get(id, options?)
Get a single application by ID.
`typescript`
const result = await client.applications.get("app-id")
if (result.ok) {
console.log(result.data.data.application)
}
#### client.applications.create(input, options?)
Create a new application.
`typescript`
const result = await client.applications.create({
name: "My Application",
applicationGroupId: "group-id", // Optional
})
#### client.applications.update(id, input, options?)
Update an existing application.
`typescript`
const result = await client.applications.update("app-id", {
name: "Updated Name",
status: "active", // "active" | "inactive"
})
#### client.applications.delete(id, options?)
Delete an application.
`typescript`
const result = await client.applications.delete("app-id")
---
Manage secrets and environment variables.
#### Secrets
##### client.env.secrets.list(options?)
List all secrets.
`typescript`
const result = await client.env.secrets.list()
if (result.ok) {
for (const secret of result.data.data.secrets) {
console.log(secret.name) // Secret values are not returned
}
}
##### client.env.secrets.get(name, options?)
Get a secret by name.
`typescript`
const result = await client.env.secrets.get("API_KEY")
##### client.env.secrets.create(input, options?)
Create a new secret.
`typescript`
const result = await client.env.secrets.create({
name: "API_KEY",
value: "secret-value",
})
##### client.env.secrets.update(name, input, options?)
Update an existing secret.
`typescript`
const result = await client.env.secrets.update("API_KEY", {
value: "new-secret-value",
})
##### client.env.secrets.delete(name, options?)
Delete a secret.
`typescript`
const result = await client.env.secrets.delete("API_KEY")
#### Variables
##### client.env.variables.list(options?)
List all environment variables.
`typescript`
const result = await client.env.variables.list()
##### client.env.variables.get(name, options?)
Get a variable by name.
`typescript`
const result = await client.env.variables.get("DATABASE_URL")
##### client.env.variables.create(input, options?)
Create a new variable.
`typescript`
const result = await client.env.variables.create({
name: "DATABASE_URL",
value: "postgres://...",
})
##### client.env.variables.update(name, input, options?)
Update an existing variable.
`typescript`
const result = await client.env.variables.update("DATABASE_URL", {
value: "postgres://new-url...",
})
##### client.env.variables.delete(name, options?)
Delete a variable.
`typescript`
const result = await client.env.variables.delete("DATABASE_URL")
---
Execute and monitor expert jobs.
#### client.jobs.list(params?, options?)
List jobs with optional filtering and pagination.
`typescript`
const result = await client.jobs.list({
take: 20,
skip: 0,
sort: "createdAt",
order: "desc",
filter: "status:running",
})
#### client.jobs.get(id, options?)
Get a job by ID.
`typescript`
const result = await client.jobs.get("job-id")
if (result.ok) {
console.log(result.data.data.job.status)
}
#### client.jobs.start(input, options?)
Start a new job.
`typescript`
const result = await client.jobs.start({
applicationId: "app-id",
expertKey: "@org/expert@1.0.0",
query: "Help me with this task",
files: ["file1.txt", "file2.txt"], // Optional
provider: "anthropic",
model: "claude-sonnet-4-20250514", // Optional
reasoningBudget: "medium", // Optional: "low" | "medium" | "high"
maxSteps: 50, // Optional
maxRetries: 3, // Optional
})
#### client.jobs.update(id, input, options?)
Update a job's status.
`typescript`
const result = await client.jobs.update("job-id", {
status: "paused",
})
#### client.jobs.continue(id, input, options?)
Continue a paused job with additional input.
`typescript`
const result = await client.jobs.continue("job-id", {
query: "Continue with this additional context",
files: ["additional-file.txt"],
interactiveToolCallResult: true, // Optional: for tool call responses
provider: "anthropic", // Optional: override provider
model: "claude-sonnet-4-20250514", // Optional: override model
maxSteps: 10, // Optional: additional steps limit
})
#### client.jobs.cancel(id, options?)
Cancel a running job.
`typescript`
const result = await client.jobs.cancel("job-id")
#### Checkpoints
Track job progress through checkpoints.
##### client.jobs.checkpoints.list(jobId, params?, options?)
List checkpoints for a job.
`typescript`
const result = await client.jobs.checkpoints.list("job-id", {
take: 50,
skip: 0,
sort: "createdAt",
order: "asc",
})
##### client.jobs.checkpoints.get(jobId, checkpointId, options?)
Get a specific checkpoint.
`typescript`
const result = await client.jobs.checkpoints.get("job-id", "checkpoint-id")
##### client.jobs.checkpoints.stream(jobId, options?)
Stream checkpoints in real-time using Server-Sent Events (SSE).
`typescript`
for await (const checkpoint of client.jobs.checkpoints.stream("job-id")) {
console.log("Activity:", checkpoint.activity)
}
---
Manage expert definitions and versions.
#### client.experts.list(params?, options?)
List available experts.
`typescript`
const result = await client.experts.list({
filter: "search term",
category: "coding", // "general" | "coding" | "research" | "writing" | "data" | "automation"
includeDrafts: false,
limit: 20,
offset: 0,
})
#### client.experts.get(key, options?)
Get an expert definition by key.
`typescript
// Get latest version
const result = await client.experts.get("@org/expert")
// Get specific version
const result = await client.experts.get("@org/expert@1.0.0")
// Get by tag
const result = await client.experts.get("@org/expert@latest")
`
#### client.experts.getMeta(key, options?)
Get expert metadata without the full definition.
`typescript`
const result = await client.experts.getMeta("@org/expert@1.0.0")
if (result.ok) {
console.log("Scope:", result.data.data.scope)
console.log("Version:", result.data.data.version)
}
#### client.experts.publish(scopeName, options?)
Publish an expert scope (make it publicly visible).
`typescript`
const result = await client.experts.publish("@org/expert")
#### client.experts.unpublish(scopeName, options?)
Unpublish an expert scope (make it private).
`typescript`
const result = await client.experts.unpublish("@org/expert")
#### client.experts.yank(key, options?)
Yank (deprecate) a specific version.
`typescript`
const result = await client.experts.yank("@org/expert@1.0.0")
if (result.ok) {
console.log("Yanked:", result.data.data.yanked)
console.log("Latest tag updated:", result.data.data.latestTagUpdated)
}
#### Drafts
Manage expert draft versions before publishing.
##### client.experts.drafts.list(scopeName, params?, options?)
List drafts for a scope.
`typescript`
const result = await client.experts.drafts.list("@org/expert", {
limit: 10,
offset: 0,
})
##### client.experts.drafts.get(scopeName, draftRef, options?)
Get a specific draft.
`typescript`
const result = await client.experts.drafts.get("@org/expert", "draft-ref")
##### client.experts.drafts.create(scopeName, input, options?)
Create a new draft.
`typescript`
const result = await client.experts.drafts.create("@org/expert", {
applicationId: "app-id",
experts: [
{
key: "main",
name: "Main Expert",
description: "A helpful assistant",
instruction: "You are a helpful assistant...",
skills: {},
delegates: [],
},
],
})
##### client.experts.drafts.update(scopeName, draftRef, input, options?)
Update an existing draft.
`typescript`
const result = await client.experts.drafts.update("@org/expert", "draft-ref", {
experts: [
{
key: "main",
name: "Updated Expert",
instruction: "Updated instructions...",
},
],
})
##### client.experts.drafts.delete(scopeName, draftRef, options?)
Delete a draft.
`typescript`
const result = await client.experts.drafts.delete("@org/expert", "draft-ref")
##### client.experts.drafts.assignVersion(scopeName, draftRef, input, options?)
Promote a draft to a versioned release.
`typescript`
const result = await client.experts.drafts.assignVersion("@org/expert", "draft-ref", {
version: "1.0.0",
tag: "latest", // Optional: assign a tag
})
#### Versions
List expert versions.
##### client.experts.versions.list(scopeName, options?)
List all versions for a scope.
`typescript${version.version}: ${version.tag || "(no tag)"}
const result = await client.experts.versions.list("@org/expert")
if (result.ok) {
for (const version of result.data.data.versions) {
console.log()`
}
}
---
The client uses a result type pattern for predictable error handling:
`typescript
const result = await client.applications.get("app-id")
if (!result.ok) {
switch (result.error.code) {
case 401:
console.error("Authentication failed")
break
case 404:
console.error("Application not found")
break
case 0:
// Network error, timeout, or validation error
if (result.error.aborted) {
console.error("Request was cancelled")
} else {
console.error("Network error:", result.error.message)
}
break
default:
console.error(Error ${result.error.code}: ${result.error.message})
}
return
}
// TypeScript knows result.data exists here
console.log(result.data.data.application)
`
Use AbortController to cancel requests:
`typescript
const controller = new AbortController()
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)
const result = await client.jobs.start(
{ applicationId: "app-id", expertKey: "@org/expert", provider: "anthropic" },
{ signal: controller.signal }
)
if (!result.ok && result.error.aborted) {
console.log("Request was cancelled")
}
`
---
For real-time checkpoint streaming, the client provides an async generator:
`typescript
import { createApiClient } from "@perstack/api-client"
const client = createApiClient({ apiKey: "your-api-key" })
// Start a job
const jobResult = await client.jobs.start({
applicationId: "app-id",
expertKey: "@org/expert",
provider: "anthropic",
})
if (!jobResult.ok) {
throw new Error(jobResult.error.message)
}
const jobId = jobResult.data.data.job.id
// Stream checkpoints
for await (const result of client.jobs.checkpoints.stream(jobId)) {
if (!result.ok) {
if (result.error.aborted) {
console.error("Stream timed out or was cancelled")
} else {
console.error("Stream error:", result.error.message)
}
break
}
console.log("Checkpoint:", result.data.id)
}
`
The streaming endpoints support an idle timeout that aborts the stream if no data is received within the specified period:
`typescript`
for await (const result of client.jobs.checkpoints.stream(jobId, {
streamIdleTimeout: 60000, // 60 second idle timeout
})) {
if (!result.ok) {
if (result.error.aborted) {
console.error("Stream timed out or was cancelled")
}
break
}
console.log("Checkpoint:", result.data.id)
}
By default, the stream idle timeout uses the client's configured timeout value (30 seconds if not specified).
For custom SSE parsing, the package exports utility functions:
`typescript
import { parseSSE, parseCheckpointSSE, parseSSEWithSchema } from "@perstack/api-client"
import { z } from "zod"
// Generic SSE parser
const reader = response.body.getReader()
for await (const event of parseSSE
console.log(event)
}
// Checkpoint-specific parser with validation
for await (const event of parseCheckpointSSE(reader)) {
if (event.type === "checkpoint") {
console.log("Checkpoint:", event.data)
} else if (event.type === "done") {
console.log("Stream complete:", event.data.status)
} else if (event.type === "error") {
console.error("Parse error:", event.data.message)
}
}
// Custom schema validation
const mySchema = z.object({ id: z.string(), value: z.number() })
for await (const event of parseSSEWithSchema(reader, mySchema)) {
console.log(event.id, event.value)
}
`
---
The package is written in TypeScript and exports all types:
`typescript
import type {
// Client types
ApiClient,
ApiClientConfig,
ApiResult,
ApiError,
RequestOptions,
StreamRequestOptions,
// Application types
Application,
ApplicationStatus,
CreateApplicationInput,
UpdateApplicationInput,
ListApplicationsParams,
// Job types
Job,
JobStatus,
StartJobInput,
UpdateJobInput,
ContinueJobInput,
Checkpoint,
CheckpointStatus,
// Expert types
Expert,
ExpertDefinition,
ExpertVersion,
ExpertScope,
} from "@perstack/api-client"
``
---
MIT