<p align="center">
  <img src="./assets/readme/amigo-banner.png" alt="Amigo banner" width="100%" />
</p>

<h1 align="center">@amigo-ai/platform-sdk</h1>

<p align="center">Official TypeScript SDK for the <a href="https://api.platform.amigo.ai/v1/docs">Amigo Platform API</a>.</p>

<p align="center">
  <a href="https://docs.amigo.ai">Product Docs</a>
  ·
  <a href="https://docs.amigo.ai/developer-guide/platform-api/platform-sdk">Developer Guide</a>
  ·
  <a href="https://docs.amigo.ai/api-reference">API Reference</a>
  ·
  <a href="./examples/README.md">Examples</a>
  ·
  <a href="./api.md">API Surface</a>
  ·
  <a href="./CHANGELOG.md">Changelog</a>
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/@amigo-ai/platform-sdk"><img src="https://img.shields.io/npm/v/@amigo-ai/platform-sdk.svg" alt="npm version" /></a>
  <a href="https://github.com/amigo-ai/amigo-platform-typescript-sdk/actions/workflows/test.yml"><img src="https://github.com/amigo-ai/amigo-platform-typescript-sdk/actions/workflows/test.yml/badge.svg" alt="CI" /></a>
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License" /></a>
</p>

Typed from the committed `openapi.json` snapshot, validated on active LTS Node releases (20, 22, and 24), and tested as packaged ESM and CommonJS tarballs before release.

## Platform context

The SDK is the typed client boundary between your runtime and the workspace-scoped Platform API. The API then fronts the platform systems that power agents, actions, calls, analytics, world state, connectors, and webhooks.

![TypeScript SDK platform context](./assets/readme/platform-architecture.svg)

## Documentation

| Need                                        | Best entry point                                                                   |
| ------------------------------------------- | ---------------------------------------------------------------------------------- |
| Product architecture and deployment context | [docs.amigo.ai](https://docs.amigo.ai/)                                            |
| Tutorials and integration guidance          | [Developer Guide](https://docs.amigo.ai/developer-guide/platform-api/platform-sdk) |
| Endpoint-by-endpoint REST reference         | [API Reference](https://docs.amigo.ai/api-reference)                               |
| Repo-local SDK examples                     | [examples/README.md](./examples/README.md)                                         |
| Generated package surface                   | [api.md](./api.md)                                                                 |
| Published release history                   | [CHANGELOG.md](./CHANGELOG.md)                                                     |

### Guides

| Guide                                                        | Description                                                     |
| ------------------------------------------------------------ | --------------------------------------------------------------- |
| [Build a Custom Patient Form](./docs/guides/build-a-form.md) | Create, deliver, and render patient intake forms using surfaces |

The docs site remains the primary reference. The repo-local examples stay close to the shipped package surface and are typechecked in CI to reduce drift.

## Installation

```bash
npm install @amigo-ai/platform-sdk
```

## Quick start

```typescript
import { AmigoClient } from '@amigo-ai/platform-sdk'

const client = new AmigoClient({
  apiKey: 'your-api-key',
  workspaceId: 'your-workspace-id',
})

// List agents
const { items: agents } = await client.agents.list({ limit: 10 })
console.log(agents.map((agent) => agent.name))

// Search entities in the world model
const entityResults = await client.world.listEntities({
  q: 'Jane Doe',
  entity_type: ['patient'],
  limit: 5,
})
console.log(entityResults.entities[0]?.display_name)

// Get call analytics for the last 30 days
const stats = await client.analytics.getCalls({ days: 30 })
console.log(stats.total_calls, stats.avg_duration_seconds)
```

## Authentication

### API key (server-to-server)

Pass `apiKey` and `workspaceId` to `AmigoClient`. Best for backend services and scripts.

### Device code flow (CLI and desktop apps)

For interactive apps where users sign in via the browser, use `loginWithDeviceCode`:

```typescript
import {
  loginWithDeviceCode,
  openBrowser,
  formatDeviceCodeInstructions,
  TokenManager,
  FileTokenStorage,
} from '@amigo-ai/platform-sdk'

const result = await loginWithDeviceCode({
  onCode: async (issuance) => {
    console.log(formatDeviceCodeInstructions(issuance))
    await openBrowser(issuance.verification_uri_complete)
  },
  onWorkspaceRequired: async (workspaces) => {
    // Prompt user to pick a workspace
    return workspaces[0].workspace_id
  },
})

// Persist credentials across runs
const tokens = new TokenManager({ storage: new FileTokenStorage() })
await tokens.store(result)

// Use the token
const client = new AmigoClient({ apiKey: result.accessToken, workspaceId: result.workspaceId })
```

See [`examples/auth/device-code-login.ts`](./examples/auth/device-code-login.ts) for a complete working example.

### Exchange an API key for a JWT

Use `client.tokens.exchangeApiKey()` to swap a long-lived API key for a
short-lived identity-issued JWT. This is useful when you want to mint a
narrowly scoped, time-bound token from a privileged server (for example to
forward to a browser via a BFF proxy, or to call the platform from a runtime
that can't safely hold the raw API key).

The call posts to `POST /token` on the configured `baseUrl` and is **not**
workspace-scoped — `workspaceId` is still required on the client because it
governs every other resource call on the same instance, but `POST /token`
itself ignores it. The SDK also unconditionally strips the configured
`Authorization` header for this one request and sends the exchange key only
as the `api_key` form field, so the configured client key is never sent over
the wire on the exchange call.

```typescript
import { AmigoClient } from '@amigo-ai/platform-sdk'

const apiKey = process.env.AMIGO_API_KEY
const workspaceId = process.env.AMIGO_WORKSPACE_ID
if (!apiKey || !workspaceId) {
  throw new Error('AMIGO_API_KEY and AMIGO_WORKSPACE_ID must be set')
}

const client = new AmigoClient({ apiKey, workspaceId })

const { access_token, expires_in, scope } = await client.tokens.exchangeApiKey({
  apiKey,
  // Optional: request a narrower scope on the issued JWT. Enforcement is
  // server-side — the SDK just forwards the value as a form field.
  scope: 'entities:read agents:read',
})

console.log(`Got JWT, expires in ${expires_in}s with scope "${scope}"`)

// Use the JWT in a second client. JWTs are passed as `apiKey` — Bearer auth.
const scopedClient = new AmigoClient({ apiKey: access_token, workspaceId })

const { items: agents } = await scopedClient.agents.list({ limit: 5 })
console.log(agents.map((agent) => agent.name))
```

The response is the standard OAuth-style token payload (`access_token`,
`token_type`, `expires_in`, `scope`, plus optional `session_id` /
`refresh_token` when applicable). The `apiKey` you pass to
`exchangeApiKey()` can be a different key than the one configured on the
client — the configured key is not used to authenticate the exchange
request itself.

### External-user sessions for customer text chat

Use external-user sessions when a customer backend needs to start or continue
text conversations on behalf of one of its own users without giving that user a
workspace membership. The customer backend first obtains a constrained parent
JWT with `external_user_sessions:create`, then mints a short-lived
`external_user` child JWT bound to one workspace, subject, and service.
For a complete setup walkthrough, see
[`docs/guides/external-user-text-conversations.md`](./docs/guides/external-user-text-conversations.md).

Create a parent external-integration credential from an admin/owner backend.
The plaintext `client_secret` is returned only once, from create or rotate:

```typescript
import { AmigoClient } from '@amigo-ai/platform-sdk'

const admin = new AmigoClient({ apiKey: process.env.AMIGO_API_KEY!, workspaceId })

const integration = await admin.externalIntegrations.create({
  name: 'customer-portal',
  display_name: 'Customer Portal',
  description: 'Backend that mints external-user chat sessions',
})

const { client_secret, credential } = await admin.externalIntegrations.createCredential(
  integration.id,
  {
    name: 'production backend',
    service_ids: [serviceId],
  },
)

console.log(credential.client_id)
// Store client_secret immediately in your secrets manager. It is shown only once.
```

At runtime, the backend sequence is:

1. Exchange `client_credentials` for the parent JWT.
2. Mint an `external_user` session for `external_subject_key`, `subject_type`,
   and `service_id`.
3. Create a conversation with the child JWT.
4. Send turns or stream turns with the child JWT.
5. Rotate the refresh token before the access token expires.

```typescript
import { EXTERNAL_USER_SESSION_CREATE_SCOPE } from '@amigo-ai/platform-sdk'

const backend = new AmigoClient({ apiKey: process.env.AMIGO_API_KEY!, workspaceId })

const parent = await backend.tokens.exchangeClientCredentials({
  clientId: process.env.AMIGO_EXTERNAL_INTEGRATION_CLIENT_ID!,
  clientSecret: process.env.AMIGO_EXTERNAL_INTEGRATION_CLIENT_SECRET!,
  scope: EXTERNAL_USER_SESSION_CREATE_SCOPE,
})

const session = await backend.tokens.createExternalUserSession({
  parentAccessToken: parent.access_token,
  externalSubjectKey: 'customer-user-123',
  subjectType: 'user',
  serviceId,
  // Optional: include only when this subject is already linked to a world entity.
  consumerEntityId,
  ttlSeconds: 1800,
})

const externalUser = new AmigoClient({
  apiKey: session.access_token,
  workspaceId,
})

const conversation = await externalUser.conversations.create({ service_id: serviceId })
await externalUser.conversations.createTurn(conversation.id, {
  message: 'Hello, I need help scheduling',
})

const refreshed = await backend.tokens.refresh({
  refreshToken: session.refresh_token!,
  workspaceId,
})
```

`external_user` tokens are intentionally conversation-scoped. They can create,
read, close, send turns, and stream turns for their own service-bound
conversation, but they cannot list workspace conversations or request
`include_tool_calls`. A service or entity mismatch is rejected by the API.
Handle `token_expired` by rotating with `client.tokens.refresh()`. Treat refresh
reuse/theft errors as terminal and restart the session from the parent
credential.

## Configuration

| Option        | Type           | Required | Description                                                          |
| ------------- | -------------- | -------- | -------------------------------------------------------------------- |
| `apiKey`      | `string`       | Yes      | API key or JWT from device code flow (Bearer auth)                   |
| `workspaceId` | `string`       | Yes      | Your workspace ID — all resource operations are scoped to this       |
| `baseUrl`     | `string`       | No       | Override the API base URL (default: `https://api.platform.amigo.ai`) |
| `retry`       | `RetryOptions` | No       | Retry configuration for transient failures                           |
| `maxRetries`  | `number`       | No       | Convenience alias for retry count                                    |
| `timeout`     | `number`       | No       | Default request timeout in milliseconds                              |
| `headers`     | `HeadersInit`  | No       | Default headers added to every request                               |
| `hooks`       | `ClientHooks`  | No       | Request/response lifecycle hooks for tracing or logging              |
| `fetch`       | `typeof fetch` | No       | Custom fetch for BFF proxy, cookie forwarding, or test mocking       |

### Retry options

```typescript
const client = new AmigoClient({
  apiKey: 'your-key',
  workspaceId: 'your-workspace-id',
  retry: {
    maxAttempts: 3, // Total attempts including first. Default: 3
    baseDelayMs: 250, // Base delay for exponential backoff. Default: 250
    maxDelayMs: 30000, // Cap on delay. Default: 30_000
  },
})
```

GET requests are retried on 408, 429, 500, 502, 503, 504. POST requests are only retried on 429 with a `Retry-After` header. Backoff uses full jitter.

### Runtime requirements

The SDK is built around web-standard primitives. Use it in runtimes that provide:

- `fetch`, `Request`, `Response`, `Headers`, `URL`
- `AbortController`
- `TextEncoder` / `TextDecoder`
- `crypto.subtle` for webhook signature verification

CI currently validates active LTS Node releases. Standards-based edge/server runtimes with the same APIs work well with the low-level request wrappers.

## Generated Types

The SDK ships with generated OpenAPI types and re-exports them for direct use:

```typescript
import type { components, operations, paths } from '@amigo-ai/platform-sdk'

type Agent = components['schemas']['AgentResponse']
type ListAgentsQuery = operations['list_agents_v1__workspace_id__agents_get']['parameters']['query']
```

Public builds are generated from the committed [`openapi.json`](./openapi.json) snapshot in this repo so type output stays deterministic across machines and CI runs. When you need to refresh that snapshot, run:

```bash
npm run openapi:sync
```

For a repo-local overview of the exported client surface, see the generated [api.md](./api.md).

## Advanced request control

The normal resource surface supports scoped request overrides, so you can keep the ergonomic API while adding timeout, retry, and header controls:

```typescript
const agents = await client
  .withOptions({
    timeout: 5_000,
    maxRetries: 1,
    headers: { 'X-Debug-Trace': 'true' },
  })
  .agents.list({ limit: 10 })

console.log(agents._request_id)
console.log(agents.lastResponse.statusCode)
console.log(agents.items)
```

You can scope options to a single resource as well:

```typescript
const agent = await client.agents.withOptions({ timeout: 2_000 }).get('agent-id')
```

For lower-level control, use the built-in typed HTTP helpers. Workspace-scoped routes automatically receive your configured `workspaceId`, and the configured value wins if `workspace_id` is provided manually.

```typescript
const result = await client.GET('/v1/{workspace_id}/agents', {
  params: { query: { limit: 10 } },
  timeout: 5_000,
  maxRetries: 1,
  headers: { 'X-Debug-Trace': 'true' },
})

console.log(result.requestId)
console.log(result.data.items)
console.log(result.rateLimit.remaining)
```

Available helpers:

- `client.GET(...)`
- `client.POST(...)`
- `client.PUT(...)`
- `client.PATCH(...)`
- `client.DELETE(...)`
- `client.HEAD(...)`
- `client.OPTIONS(...)`
- `client.withOptions(...)`
- `client.<resource>.withOptions(...)`

### Response metadata

Object responses from resource methods include non-enumerable request metadata:

```typescript
const agent = await client.agents.get('agent-id')

console.log(agent._request_id)
console.log(agent.lastResponse.statusCode)
console.log(agent.lastResponse.rateLimit.remaining)
```

Low-level request helpers return the raw `Response` alongside parsed data:

```typescript
const { data, response, requestId } = await client.GET('/v1/{workspace_id}/agents')

console.log(requestId)
console.log(response.headers.get('content-type'))
console.log(data.items)
```

### Request hooks

Use hooks for logging, tracing, and metrics without wrapping `fetch` yourself:

```typescript
const client = new AmigoClient({
  apiKey: 'your-api-key',
  workspaceId: 'your-workspace-id',
  hooks: {
    onRequest({ request, schemaPath }) {
      console.log('request', request.method, schemaPath)
    },
    onResponse({ response, requestId }) {
      console.log('response', response.status, requestId)
    },
  },
})
```

## Resources

### Tokens

Exchange a long-lived API key for a short-lived identity-issued JWT, or mint
external-user session tokens for customer text chat. See
[Exchange an API key for a JWT](#exchange-an-api-key-for-a-jwt) and
[External-user sessions for customer text chat](#external-user-sessions-for-customer-text-chat)
under Authentication for the full walkthroughs.

```typescript
import { EXTERNAL_USER_SESSION_CREATE_SCOPE } from '@amigo-ai/platform-sdk'

const { access_token, expires_in } = await client.tokens.exchangeApiKey({
  apiKey: process.env.AMIGO_API_KEY!,
  scope: 'entities:read agents:read',
})

const parent = await client.tokens.exchangeClientCredentials({
  clientId: process.env.AMIGO_EXTERNAL_INTEGRATION_CLIENT_ID!,
  clientSecret: process.env.AMIGO_EXTERNAL_INTEGRATION_CLIENT_SECRET!,
  scope: EXTERNAL_USER_SESSION_CREATE_SCOPE,
})
```

### Agents

```typescript
import type { VoiceSessionProvider } from '@amigo-ai/platform-sdk'

// Create an agent
const agent = await client.agents.create({
  name: 'Patient Intake Agent',
  description: 'Handles inbound scheduling calls',
})

// Create a version (the versioned config object)
const version = await client.agents.createVersion(agent.id, {
  name: 'v1',
  identity: {
    name: 'Alex',
    role: 'Scheduling Coordinator',
    developed_by: 'Acme Health',
    default_spoken_language: 'en',
    relationship_to_developer: {
      ownership: 'Acme Health',
      type: 'assistant',
      conversation_visibility: 'public',
      thought_visibility: 'private',
    },
  },
  voice_config: {
    voice_id: 'voice-abc123',
    session_provider: 'inhouse' satisfies VoiceSessionProvider,
  },
})

// Get the latest version
const latest = await client.agents.getVersion(agent.id, 'latest')

const { items: agents } = await client.agents.list({ search: 'intake' })
```

### Actions

Actions are reusable agent capabilities (formerly "skills").

```typescript
const action = await client.actions.create({
  slug: 'schedule-appointment',
  name: 'Schedule Appointment',
  description: 'Books appointments in the scheduling system',
  input_schema: {
    type: 'object',
    properties: {
      patient_id: { type: 'string' },
      appointment_type: { type: 'string' },
    },
    required: ['patient_id', 'appointment_type'],
  },
})

// Test with a sample input
const result = await client.actions.test(action.id, {
  input: { patient_id: 'ID-001', appointment_type: 'follow-up' },
})
console.log(result.result, result.duration_ms)
```

### Services

Services wire together an agent + context graph + phone channel.

```typescript
import type { VoiceSessionProvider } from '@amigo-ai/platform-sdk'

const { items: services } = await client.services.list()
const service = await client.services.get('service-id')
console.log(service.agent_name, service.channel_type, service.version_sets)

await client.services.update(service.id, {
  voice_config: {
    ...(service.voice_config ?? {}),
    session_provider: 'inhouse' satisfies VoiceSessionProvider,
  },
})
```

### World Model

The world model tracks entities (patients, contacts, appointments) and the events that flow through them.

```typescript
// Filter entities with simple list queries
const patients = await client.world.listEntities({
  q: 'Jane Doe',
  entity_type: ['patient'],
  limit: 10,
})
console.log(patients.entities.length)

// Get a single entity
const patient = await client.world.getEntity('entity-id')
console.log(patient.display_name, patient.entity_type)

// Query timeline
const timeline = await client.world.getTimeline('entity-id', { limit: 20 })

// Semantic search over the world model
const results = await client.world.search({
  q: 'Jane Doe',
  entity_type: 'patient',
  limit: 5,
})

// View sync status from connectors
const syncStatus = await client.world.getSyncStatusBySink()
```

### Calls

Calls are read-only — they are created by the voice pipeline.

```typescript
const { items: calls } = await client.calls.list({
  direction: 'inbound',
  service_id: 'service-id',
})

// Get full detail with transcript and intelligence
const detail = await client.calls.get(calls[0].call_sid)
console.log(detail.intelligence?.summary)
console.log(detail.transcript)

// Analytics benchmarks
const benchmarks = await client.calls.getBenchmarks({ days: 30 })
```

### Text conversations

Use `client.conversations.sendMessage()` for user-first synchronous text turns. Omit
`conversation_id` to start a new durable conversation; pass the returned ID to resume it.

```typescript
const firstTurn = await client.conversations.sendMessage({
  service_id: 'service-id',
  message: 'Hello, I need help scheduling',
  entity_id: 'entity-id',
})

const nextTurn = await client.conversations.sendMessage({
  service_id: 'service-id',
  conversation_id: firstTurn.conversation_id,
  message: 'Tuesday morning works',
})

console.log(nextTurn.messages.map((message) => message.text))
```

For real-time browser clients, build the text-stream URL and use WebSocket
subprotocol auth so the token is not placed in the URL:

```typescript
import { textStreamAuthProtocols } from '@amigo-ai/platform-sdk'

const apiKey = process.env.AMIGO_API_KEY!
const url = client.conversations.textStreamUrl({ serviceId: 'service-id' })
const socket = new WebSocket(url, textStreamAuthProtocols(apiKey))

socket.addEventListener('open', () => {
  socket.send(JSON.stringify({ type: 'message', text: 'Hello' }))
})
```

If a browser rejects your API key as a WebSocket subprotocol value, use the
query-token fallback only in trusted contexts. URL tokens can appear in browser
history, server access logs, HTTP proxy logs, and referrer headers:

```typescript
// WARNING: query tokens can be captured by URL logs and browser history.
const url = client.conversations.textStreamUrl({ serviceId: 'service-id', token: apiKey })
const socket = new WebSocket(url)
```

### Real-time event streams

For workspace-wide events (calls, surfaces, pipeline, operators, channels), use the typed SSE consumer:

```typescript
const handle = client.events.subscribeToWorkspace({
  onEvent: (event) => {
    switch (event.event_type) {
      case 'call.started':
        console.log('call started:', event.call_sid)
        break
      case 'pipeline.error':
        console.error('pipeline error:', event)
        break
    }
  },
  onError: (err) => console.error('terminal:', err),
  onReconnect: (attempt) => console.warn(`reconnect #${attempt}`),
})

// Later: handle.unsubscribe(); await handle.done
```

The helper handles automatic reconnect (exp backoff with jitter, server-sent
`retry:` honored), gapless replay via `Last-Event-ID`, and discriminated-union
dispatch. Server emits a structured `error` frame with a stable `code` on
terminal failures — narrow with the typed error guard:

```typescript
import { isWorkspaceEventStreamError } from '@amigo-ai/platform-sdk'

onError: (err) => {
  if (isWorkspaceEventStreamError(err)) {
    if (err.code === 'too_many_streams') {
      // workspace has too many open streams; close another tab
    } else if (err.retryable) {
      // SDK already retried up to maxReconnects; safe to try again later
    }
  }
}
```

For per-call voice observation (`agent_transcript_delta`, `latency`, `session_*`,
`participant_*`, etc.), use the WebSocket-based observer helper:

```typescript
const observerHandle = client.observers.subscribe({
  callSid: 'CAxxx',
  token: bearerToken,
  onEvent: (event) => {
    switch (event.type) {
      case 'agent_transcript_delta':
        renderAgentDelta(event.delta)
        break
      case 'session_end':
        showSummary(event)
        break
    }
  },
  onError: (err) => console.error('observer terminal:', err.reason, err.closeCode),
})
```

Both helpers are built on the shared `ReconnectingWebSocket` primitive, which
maps platform-specific WebSocket close codes (4001 client error, 4029 rate
limit, 4403 auth, 4100 token expired) to a typed reason taxonomy and applies an
idle watchdog (default 45s) so a half-dead socket forces a reconnect rather
than hanging indefinitely. Compose it directly via `createReconnectingWebSocket`
when you need a managed WebSocket outside these resources.

### Analytics

```typescript
// Dashboard KPIs with period-over-period deltas
const dashboard = await client.analytics.getDashboard({ days: 7 })
console.log(dashboard.call_volume.value, dashboard.call_volume.delta_pct)
console.log(dashboard.avg_quality.value)

// Call volume time series
const calls = await client.analytics.getCalls({ days: 30, interval: '1d' })
console.log(calls.total_calls, calls.calls_by_date)

// Per-agent performance
const { agents } = await client.analytics.getAgents({ period: '7d' })

// Compare two periods
const comparison = await client.analytics.compareCallPeriods({
  current_from: '2026-04-01',
  current_to: '2026-04-15',
  previous_from: '2026-03-15',
  previous_to: '2026-03-31',
})
```

### Integrations

```typescript
const { items: integrations } = await client.integrations.list({ enabled: true })

// Test a specific endpoint
const result = await client.integrations.testEndpoint('integration-id', 'geocode', {
  textQuery: '123 Main St, Springfield',
})
```

### Data Sources

```typescript
const { items: sources } = await client.dataSources.list()
const source = await client.dataSources.get('source-id')
console.log(source.source_type, source.health_status, source.last_sync_at)
```

### Settings

```typescript
import type { SttProvider, TtsProvider } from '@amigo-ai/platform-sdk'

// Voice
const voice = await client.settings.voice.get()
await client.settings.voice.update({
  voice_id: 'new-voice-id',
  speed: 1.1,
  stt_provider: 'deepgram' satisfies SttProvider,
  tts_provider: 'cartesia' satisfies TtsProvider,
})

// Retention
const retention = await client.settings.retention.get()
await client.settings.retention.update({ call_recordings_days: 90 })
```

### Surfaces (Patient Forms)

Surfaces are workspace-scoped form specs for collecting patient data. Create a form, deliver it via SMS or email, and track completion. See the full guide: [Build a Custom Patient Form](./docs/guides/build-a-form.md).

```typescript
// Create a patient intake form
const surface = await client.POST('/v1/{workspace_id}/surfaces', {
  body: {
    entity_id: patientId,
    title: 'New Patient Intake',
    channel: 'sms',
    fields: [
      { key: 'full_name', label: 'Full Name', field_type: 'text', required: true },
      { key: 'date_of_birth', label: 'Date of Birth', field_type: 'date', required: true },
      {
        key: 'allergies',
        label: 'Allergies',
        field_type: 'multiselect',
        options: ['Penicillin', 'Sulfa', 'None'],
      },
    ],
  },
})
console.log(surface.data.url) // Patient-facing token URL

// Deliver via SMS
await client.POST('/v1/{workspace_id}/surfaces/{surface_id}/deliver', {
  params: { path: { surface_id: surface.data.id } },
  body: { channel_address: '+15551234567' },
})

// Track completion
const { data: rates } = await client.GET(
  '/v1/{workspace_id}/analytics/surfaces/completion-rates',
  {},
)
```

Public token routes (`/s/{token}/spec`, `/s/{token}/submit`, etc.) require no API key -- use `openapi-fetch` with the SDK's `paths` type for full type safety on unauthenticated endpoints.

### Billing

```typescript
const dashboard = await client.billing.getDashboard()
const usage = await client.billing.getUsage()
const { items: invoices } = await client.billing.listInvoices()
const pdf = await client.billing.getInvoicePdf('invoice-id')
```

### Operators

```typescript
const { items: operators } = await client.operators.list()
const dashboard = await client.operators.getDashboard()
const queue = await client.operators.getQueue()
const escalations = await client.operators.getActiveEscalations()

// Join/leave calls, switch mode, send guidance
await client.operators.joinCall('operator-id', { call_sid: 'call-sid' })
await client.operators.sendGuidance('operator-id', { text: 'Ask about allergies' })
await client.operators.wrapUp('operator-id', { outcome: 'resolved' })
```

### Triggers (Automations)

```typescript
const trigger = await client.triggers.create({
  name: 'Daily outreach',
  schedule: '0 9 * * 1-5',
  timezone: 'America/New_York',
  action_id: 'skill-id',
  event_type: 'trigger.scheduled',
  input_template: { campaign: 'follow-up' },
})

await client.triggers.fire(trigger.id)
await client.triggers.pause(trigger.id)
await client.triggers.resume(trigger.id)
const runs = await client.triggers.listRuns(trigger.id)
```

### Review Queue

```typescript
const stats = await client.reviewQueue.getStats()
const dashboard = await client.reviewQueue.getDashboard()
const { items } = await client.reviewQueue.list({ status: 'pending' })

// Claim, approve, reject, correct
await client.reviewQueue.claim('item-id')
await client.reviewQueue.approve('item-id', { notes: 'Verified correct' })
await client.reviewQueue.reject('item-id', { reason: 'Data mismatch' })
await client.reviewQueue.batchApprove({ item_ids: ['id1', 'id2'] })
```

### Compliance

```typescript
const hipaa = await client.compliance.getHipaa()
```

### Audit

```typescript
const { items: events } = await client.audit.list({ limit: 50 })
const summary = await client.audit.getSummary()
await client.audit.createExport({ start_date: '2026-01-01', end_date: '2026-03-31' })
```

### Recordings

```typescript
const urls = await client.recordings.getUrls('call-sid')
const metadata = await client.recordings.getMetadata('call-sid')
```

### Functions (UC Functions)

```typescript
const { items: functions } = await client.functions.list()
const fn = await client.functions.get('my-function')
const result = await client.functions.test('my-function', { input: { query: 'test' } })
```

### Workspace Data Queries

```typescript
const query = await client.workspaceDataQueries.create({
  name: 'recent_orders',
  description: 'Recent orders by status',
  sql_template: 'select * from custom.orders where status = :status',
  parameters: [{ name: 'status', type: 'string', description: 'Order status' }],
})
const result = await client.workspaceDataQueries.invoke(query.id, {
  input: { status: 'open' },
})
```

## Webhook Verification

Use the raw request body when verifying webhook deliveries. Timestamped signatures are replay-protected by default.

```typescript
import { parseWebhookEvent, WebhookVerificationError } from '@amigo-ai/platform-sdk'

const body = await request.text()

try {
  const event = await parseWebhookEvent({
    payload: body,
    signature: request.headers.get('x-amigo-signature') ?? '',
    timestamp: request.headers.get('x-amigo-timestamp') ?? undefined,
    secret: process.env.AMIGO_WEBHOOK_SECRET!,
  })

  console.log(event.type, event.data)
} catch (error) {
  if (error instanceof WebhookVerificationError) {
    console.error('Rejected webhook:', error.message)
  } else {
    throw error
  }
}
```

If your delivery channel only provides a legacy HMAC without a timestamp, the original helper signature still works:

```typescript
import { parseWebhookEvent } from '@amigo-ai/platform-sdk'

const event = await parseWebhookEvent(rawBody, signature, secret)
```

## BFF Proxy (Next.js)

For frontend apps that use a Backend-for-Frontend proxy:

```typescript
const client = new AmigoClient({
  apiKey: 'bff-proxy',
  workspaceId: 'ws-id',
  baseUrl: '/api/platform',
  fetch: customFetchWithCookies,
})
```

## Pagination

The SDK now exposes first-class async auto-pagination helpers on collection resources:

```typescript
import { AmigoClient } from '@amigo-ai/platform-sdk'

for await (const agent of client.agents.listAutoPaging({ limit: 100 })) {
  console.log(agent.name)
}

for await (const entity of client.world.listEntitiesAutoPaging({ limit: 100 })) {
  console.log(entity.display_name)
}
```

For custom pagination flows, the lower-level `paginate(...)` utility remains available.

## Error handling

All SDK errors extend `AmigoError`. Use type guards for specific handling:

```typescript
import {
  AmigoClient,
  AmigoError,
  isNotFoundError,
  isRateLimitError,
  isAuthenticationError,
} from '@amigo-ai/platform-sdk'

try {
  await client.agents.get('agent-id')
} catch (err) {
  if (isNotFoundError(err)) {
    console.log('Agent not found')
  } else if (isRateLimitError(err)) {
    console.log('Rate limited, retry after:', err.retryAfter, 'seconds')
  } else if (isAuthenticationError(err)) {
    console.log('Invalid API key')
  } else if (err instanceof AmigoError) {
    console.log('API error:', err.message, err.errorCode, err.requestId)
  }
}
```

Webhook verification errors are separate from API transport errors and throw `WebhookVerificationError`.

### Error classes

| Class                 | HTTP Status | Description                             |
| --------------------- | ----------- | --------------------------------------- |
| `BadRequestError`     | 400         | Malformed request                       |
| `AuthenticationError` | 401         | Invalid or expired API key              |
| `PermissionError`     | 403         | Insufficient permissions                |
| `NotFoundError`       | 404         | Resource does not exist                 |
| `ConflictError`       | 409         | Duplicate slug or version conflict      |
| `ValidationError`     | 422         | Request body validation failure         |
| `RateLimitError`      | 429         | Too many requests — check `.retryAfter` |
| `ServerError`         | 5xx         | Server-side error                       |
| `ConfigurationError`  | —           | SDK misconfiguration at init time       |
| `NetworkError`        | —           | Fetch/network failure                   |
| `RequestTimeoutError` | —           | Request exceeded the configured timeout |

## CommonJS (CJS) usage

```javascript
const { AmigoClient } = require('@amigo-ai/platform-sdk')
```

## License

MIT
