TypeScript Guide
How to call the Amdahl Platform API from TypeScript (Node.js 20+ or modern browsers). The platform does not ship a generated SDK yet; native fetch is enough for every operation. A typed SDK is on the roadmap; this guide is the recommended path until then.
Everything below uses fetch, crypto.subtle / node:crypto, and no external dependencies.
Setup
npm install --save-dev typescript @types/nodetsconfig.json target should be ES2022 or later so native fetch is recognized. For Node 20, add "types": ["node"].
Environment:
const AMDAHL_URL = process.env.AMDAHL_URL!
const API_KEY = process.env.API_KEY!
const BASE = `https://app.amdahl.co/api/platform/v1`Core helper
A single api() wrapper that attaches auth, parses JSON, and surfaces typed errors. Every example below uses it.
export class AmdahlError extends Error {
constructor(
public readonly status: number,
public readonly code: string,
message: string,
public readonly body?: unknown
) {
super(message)
this.name = 'AmdahlError'
}
}
interface ErrorEnvelope {
error?: { code?: string; message?: string }
}
export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
const headers = new Headers(init.headers)
headers.set('Authorization', `Bearer ${API_KEY}`)
if (init.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json')
}
const res = await fetch(`${BASE}${path}`, { ...init, headers })
const text = await res.text()
const json: unknown = text.length > 0 ? JSON.parse(text) : null
if (!res.ok) {
const env = (json ?? {}) as ErrorEnvelope
throw new AmdahlError(
res.status,
env.error?.code ?? 'unknown',
env.error?.message ?? `HTTP ${res.status}`,
json
)
}
return json as T
}Shared types
These mirror the response shapes documented in api-reference/README.md. Narrow them further for your own app as needed.
export interface QueryResult {
columns: string[]
rows: Array<Record<string, unknown>>
row_count: number
duration_ms: number
}
export interface Page {
id: string
name: string
slug: string
spec: Record<string, unknown>
declared_queries: Array<Record<string, unknown>>
tags: string[]
created_at: string
updated_at: string
}
export interface Paginated<T> {
items?: T[]
next_cursor: string | null
}
export interface AgentSession {
session_id: string
status: 'queued' | 'running' | 'awaiting_input' | 'complete' | 'failed' | 'canceled'
turn_count?: number
current_tool?: string | null
pending_input_type?: string | null
pending_input_payload?: Record<string, unknown>
result_summary?: string
result_artifact_id?: string | null
updated_at: string
}Data queries
data.query runs read-only SQL against your interactions table, auto-scoped to your business. It is a POST /data/query and requires data:read.
export function queryData(sql: string): Promise<QueryResult> {
return api('/data/query', { method: 'POST', body: JSON.stringify({ sql }) })
}
// Example:
// const res = await queryData(
// 'SELECT channel, COUNT(*) AS events FROM interactions GROUP BY channel ORDER BY events DESC LIMIT 5'
// )
// for (const row of res.rows) console.log(row.channel, row.events)Pages
A Page is a workspace-authored data UI: a catalog-component spec plus named queries the host runs server-side. create and update require pages:write; the reads require pages:read.
export function listPages(): Promise<{ items: Page[] }> {
return api('/pages')
}
export function getPage(id: string): Promise<Page> {
return api(`/pages/${id}`)
}
export interface CreatePageInput {
name: string
spec: Record<string, unknown>
declared_queries?: Array<Record<string, unknown>>
summary?: string
tags?: string[]
slug?: string
}
export function createPage(input: CreatePageInput): Promise<Page> {
return api('/pages', { method: 'POST', body: JSON.stringify(input) })
}
export function updatePage(id: string, patch: Partial<CreatePageInput>): Promise<Page> {
return api(`/pages/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
}
export async function archivePage(id: string): Promise<void> {
await api(`/pages/${id}`, { method: 'DELETE' })
}Agents
Start and poll
export interface StartAgentInput {
profile_id: string
task: string
input_params?: Record<string, unknown>
async?: boolean
}
export function startAgent(input: StartAgentInput): Promise<AgentSession> {
return api('/agents/run', { method: 'POST', body: JSON.stringify(input) })
}
export function getAgentStatus(sessionId: string): Promise<AgentSession> {
return api(`/agents/${sessionId}/status`)
}
export function resumeAgent(
sessionId: string,
input: Record<string, unknown>
): Promise<AgentSession> {
return api(`/agents/${sessionId}/resume`, {
method: 'POST',
body: JSON.stringify({ input }),
})
}
const TERMINAL = new Set(['complete', 'failed', 'canceled'])
export async function waitForAgent(
sessionId: string,
opts: { intervalMs?: number; timeoutMs?: number } = {}
): Promise<AgentSession> {
const interval = opts.intervalMs ?? 3000
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60_000)
for (;;) {
const s = await getAgentStatus(sessionId)
if (TERMINAL.has(s.status) || s.status === 'awaiting_input') return s
if (Date.now() > deadline) throw new Error(`agent ${sessionId} timed out`)
await new Promise(r => setTimeout(r, interval))
}
}Stream progress (SSE)
Native fetch streaming with a small SSE parser. Works in Node 20 and browsers.
export interface StreamEvent {
type: string
data: Record<string, unknown>
}
export async function* streamAgent(
sessionId: string,
signal?: AbortSignal
): AsyncGenerator<StreamEvent> {
const res = await fetch(`${BASE}/agents/${sessionId}/stream`, {
headers: { Authorization: `Bearer ${API_KEY}` },
signal,
})
if (!res.ok || !res.body) {
throw new AmdahlError(res.status, 'stream_failed', `stream ${res.status}`)
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
try {
for (;;) {
const { value, done } = await reader.read()
if (done) return
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split('\n\n')
buffer = frames.pop() ?? ''
for (const frame of frames) {
const line = frame.split('\n').find(l => l.startsWith('data: '))
if (!line) continue
yield JSON.parse(line.slice(6)) as StreamEvent
}
}
} finally {
reader.cancel().catch(() => {})
}
}
// Example usage:
// for await (const evt of streamAgent(sessionId)) {
// console.log(evt.type, evt.data)
// if (evt.type === 'session.completed') break
// }Webhooks
Register and manage
export interface Webhook {
id: string
url: string
events: string[]
enabled: boolean
secret?: string
created_at: string
}
export function registerWebhook(input: {
url: string
events: string[]
description?: string
}): Promise<Webhook> {
return api('/webhooks', { method: 'POST', body: JSON.stringify(input) })
}
export function listWebhooks(): Promise<{ webhooks: Webhook[] }> {
return api('/webhooks')
}
export function updateWebhook(id: string, patch: Partial<Webhook>): Promise<Webhook> {
return api(`/webhooks/${id}`, { method: 'PATCH', body: JSON.stringify(patch) })
}
export async function deleteWebhook(id: string): Promise<void> {
await api(`/webhooks/${id}`, { method: 'DELETE' })
}HMAC verification (Node)
Use on your receiver. Capture the raw bytes before any JSON parser touches them.
import crypto from 'node:crypto'
export function verifyWebhookSignature(
rawBody: Buffer,
signatureHeader: string | undefined,
secret: string
): boolean {
if (!signatureHeader) return false
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(signatureHeader, 'hex')
if (a.length !== b.length) return false
return crypto.timingSafeEqual(a, b)
}Full receiver example: recipes/react-to-webhook.md.
Error handling pattern
The AmdahlError class gives you structured access to the status code, machine-readable code, and the raw response body. Branch on code, not message.
try {
await startAgent({ profile_id: 'content_writer', task: '' })
} catch (err) {
if (err instanceof AmdahlError) {
if (err.code === 'invalid_argument') {
console.error('bad input:', err.message)
} else if (err.status === 429) {
const reset = Number(err.body && (err.body as any).retry_after) || 5
console.error(`rate limited; retry in ${reset}s`)
} else {
console.error('amdahl error', err.status, err.code, err.message)
}
return
}
throw err
}Full code reference: api-reference/errors.md.
Retry helper
Retry on 429 and 5xx with exponential backoff. Wrap any api() call.
export async function withRetry<T>(
fn: () => Promise<T>,
opts: { maxAttempts?: number; baseMs?: number } = {}
): Promise<T> {
const max = opts.maxAttempts ?? 4
const base = opts.baseMs ?? 500
let lastErr: unknown
for (let attempt = 1; attempt <= max; attempt++) {
try {
return await fn()
} catch (err) {
lastErr = err
const retryable = err instanceof AmdahlError && (err.status === 429 || err.status >= 500)
if (!retryable || attempt === max) throw err
const delay = base * 2 ** (attempt - 1) + Math.random() * 200
await new Promise(r => setTimeout(r, delay))
}
}
throw lastErr
}Pagination pattern
Cursor-paginated list endpoints return next_cursor (null at the end). The same async-iterator pattern works against any of them; swap in the resource path and the array key it returns:
export async function* iterate<T>(path: string, key = 'items'): AsyncGenerator<T> {
let cursor: string | undefined
for (;;) {
const qs = new URLSearchParams({ limit: '100' })
if (cursor) qs.set('cursor', cursor)
const page = (await api(`${path}?${qs}`)) as Paginated<T> & Record<string, T[]>
for (const row of page[key] ?? []) yield row
if (!page.next_cursor) return
cursor = page.next_cursor
}
}For webhooks (currently unpaginated) and other small resources, a single call is enough.
Tips
- Keep secrets out of client code. Never expose
API_KEYto the browser; run calls server-side and proxy for the browser if needed. - Set AbortController on long polls and streams. Cancel them when the user navigates away or your process shuts down.
- Prefer
async=true+ polling or SSE overasync=false. The 60-second sync wait times out easily on slower runs. - Cache read-heavy responses by a stable key. For example, key a
data.queryresult by its SQL string when the underlying data only changes on a known cadence. - Version lock in your deploy: pin to the API version header (
X-Amdahl-API-Version) documented in versioning.md so a server upgrade never surprises you.
See also
- quickstart.md for your first end-to-end call.
- sdk/curl.md for the raw HTTP equivalent of every helper here.
- sdk/python.md for the Python equivalent.
- recipes/draft-blog-post.md for an end-to-end workflow using these helpers.
- api-reference/README.md for the full endpoint catalog.