Amy
SDKs

SDK, TypeScript

The TypeScript SDK is the canonical client. It works in Bun, Node, Deno, browsers, and React Native. Every other language SDK is generated from the same OpenAPI spec.

Install

bun add @amy/sdk           # bun
npm install @amy/sdk       # npm
pnpm add @amy/sdk          # pnpm
yarn add @amy/sdk          # yarn

Initialize

import { Amy } from "@amy/sdk";

const amy = new Amy({
  apiKey: process.env.AMY_API_KEY!,
  baseUrl: process.env.AMY_BASE_URL ?? "https://api.amy.health",

  // Optional
  timeout: 60_000,           // ms; default 60s for non-streaming, infinite for streams
  maxRetries: 2,             // default 2; retries 429/5xx with exponential backoff
  fetch: globalThis.fetch,   // override for custom fetch (e.g. polyfills)
});

Auth

The apiKey is sent as Authorization: Bearer <apiKey> on every request. You can rotate it any time:

amy.setApiKey("amy_live_new_key");

For unauthenticated requests (rare, only the CLI device-flow start), omit apiKey.


Resources

Every resource lives at amy.<resource> and follows the same method shape. Listed here in surface order.

amy.turns, Agent runs

// Start a turn
const turn = await amy.turns.create({
  messages: [{ role: "user", content: "How's my HRV this month?" }],
  stream: true,    // default true; if false, blocks until complete
});

// Get status + result
const status = await amy.turns.retrieve(turn.id);

// Stream events (async iterator)
for await (const event of amy.turns.stream(turn.id)) {
  switch (event.type) {
    case "turn.started":      console.log("Turn started"); break;
    case "agent.started":     console.log("Agent:", event.agent); break;
    case "agent.thought":     process.stdout.write(event.delta); break;
    case "agent.completed":   console.log("\n[", event.agent, "done ]"); break;
    case "validator.gate":    console.log("Gate:", event.gate, event.verdict); break;
    case "turn.completed":    console.log(event.result.answer); break;
    case "turn.failed":       console.error(event.error.message); break;
  }
}

// List turns (cursor pagination — auto-paginates with .iter())
for await (const turn of amy.turns.list().iter()) {
  console.log(turn.id, turn.status);
}

// Or get just one page
const page = await amy.turns.list({ limit: 10 });
console.log(page.data, page.has_more);

// Get raw SSE init (for environments where EventSource needs the
// URL + headers, like React Native)
const { url, headers } = amy.turns.eventSourceInit(turn.id);

amy.sources, Connected wearables

const list = await amy.sources.list();

const { widget_url } = await amy.sources.terra.connect({
  redirect_url: "https://your-app/oauth/callback",
});
// Open widget_url in a browser; Terra handles the rest.

await amy.sources.disconnect("src_…");

amy.labs, Bloodwork uploads

// Upload a PDF or image. `file` can be a Blob, File, or ReadableStream.
const lab = await amy.labs.upload({
  file: fileBlob,
  filename: "panel.pdf",   // optional; inferred from File.name when present
});

// Status + parsed biomarkers
const parsed = await amy.labs.retrieve(lab.id);
console.log(parsed.status, parsed.biomarkers);

// List
const list = await amy.labs.list();

amy.data, Sync and query

// Delta sync (for offline-first clients)
const delta = await amy.data.sync({ cursor: lastCursor });
storeLocally(delta);
lastCursor = delta.next_cursor;

// Biomarker timeseries
const hrv = await amy.data.biomarkers({
  name: "hrv_ms",
  from: "2026-01-01",
  to: "2026-05-25",
});

// Daily summary
const day = await amy.data.summaries.retrieve("2026-05-24");

amy.memory, What Amy remembers

const facts = await amy.memory.list();

const fact = await amy.memory.create({
  text: "Vegetarian. No fish.",
  category: "preference",
});

await amy.memory.delete(fact.id);

amy.me, Current user

const me = await amy.me.get();
await amy.me.update({ name: "Yat" });

Streaming protocol details

amy.turns.stream(id) returns an AsyncIterable<TurnEvent>. Under the hood it uses SSE; in browsers and React Native it falls back to EventSource if available, otherwise it parses the stream manually from fetch.

Reconnection

The iterator transparently reconnects on transient errors with Last-Event-Id. You don't need to handle this. To see what's happening:

const iterator = amy.turns.stream(turn.id, { onReconnect: (lastId) => {
  console.log("Reconnecting from event", lastId);
}});

Cancellation

const controller = new AbortController();
const iterator = amy.turns.stream(turn.id, { signal: controller.signal });

setTimeout(() => controller.abort(), 5_000);
// iterator throws AbortError

Event types

event.typePayloadWhen
turn.started{ turn_id, at }Once at the start
agent.started{ agent, at }When each agent begins
agent.thought{ agent, delta }For every token chunk
agent.completed{ agent, output_summary }When each agent finishes
validator.gate{ gate, verdict, evidence }Each validation gate verdict
validator.critic{ verdict, reasoning }Critic's adversarial review
turn.completed{ turn_id, result }Once at the end (success)
turn.failed{ turn_id, error }Once at the end (failure)

Full schema for each: Concepts: Streaming.


Errors

Every method throws a typed AmyError on non-2xx responses.

import { Amy, AmyError, errors } from "@amy/sdk";

try {
  await amy.turns.retrieve("turn_does_not_exist");
} catch (err) {
  if (err instanceof errors.NotFoundError) {
    // err.code = "turn_not_found"
    // err.requestId = "req_…"
    // err.docsUrl  = "https://docs.amy.health/concepts/errors#turn_not_found"
  }
  if (err instanceof errors.RateLimitError) {
    // err.retryAfter = seconds
  }
  if (err instanceof AmyError) {
    // base class for all SDK errors
  }
}

Error hierarchy:

AmyError
├── ValidationError       (400)
├── AuthenticationError   (401)
├── PermissionError       (403)
├── NotFoundError         (404)
├── ConflictError         (409)
├── UnprocessableError    (422)
├── RateLimitError        (429)
├── InternalError         (500)
└── UpstreamError         (502/503/504)

Idempotency

Every write call auto-generates an Idempotency-Key (UUIDv4). Override it to make retries safe across processes:

const turn = await amy.turns.create(
  { messages: [...] },
  { idempotencyKey: "user-action-abc-123" },
);

If you POST the same key twice within 24 hours, the second call returns the cached response from the first.


Pagination

Every list method returns a page wrapper. Two ways to consume:

// Manual paging
const page1 = await amy.turns.list({ limit: 50 });
const page2 = await amy.turns.list({ limit: 50, cursor: page1.next_cursor });

// Auto-paging (handles cursors for you)
for await (const turn of amy.turns.list().iter()) {
  // walks through all turns
}

React Native notes

  • The SDK works out of the box on Expo and bare React Native.

  • For streaming, install react-native-event-source and pass it via EventSourceCtor:

    import EventSource from "react-native-event-source";
    const amy = new Amy({ apiKey, EventSourceCtor: EventSource });
  • For file uploads from expo-document-picker, pass the picker's assets[0] directly:

    const lab = await amy.labs.upload({ file: result.assets[0] });

Browser notes

  • The SDK uses native fetch and EventSource.
  • CORS is enabled on the API for any origin (with the appropriate headers); your API key must be passed via the Authorization header, not a cookie.
  • For long streams, consider running the API call from a Web Worker so the main thread stays responsive.

Testing

The SDK has zero side effects on import. To mock it in tests:

import { Amy } from "@amy/sdk";
import { vi } from "vitest";

vi.mock("@amy/sdk");

const mockedAmy = vi.mocked(new Amy({ apiKey: "test" }));
mockedAmy.turns.create.mockResolvedValue({ id: "turn_test", status: "queued", … });

For end-to-end tests against a real backend, use a dedicated test user and the AMY_TEST_BASE_URL environment variable.


Where to next

On this page