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 # yarnInitialize
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 AbortErrorEvent types
event.type | Payload | When |
|---|---|---|
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-sourceand pass it viaEventSourceCtor:import EventSource from "react-native-event-source"; const amy = new Amy({ apiKey, EventSourceCtor: EventSource }); -
For file uploads from
expo-document-picker, pass the picker'sassets[0]directly:const lab = await amy.labs.upload({ file: result.assets[0] });
Browser notes
- The SDK uses native
fetchandEventSource. - CORS is enabled on the API for any origin (with the appropriate
headers); your API key must be passed via the
Authorizationheader, 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
- API reference, every endpoint, every shape.
- Concepts: Streaming, full event catalog.
- Recipe: Build a mobile app, see the SDK in a real app.
SDKs
One contract, one spec, many languages. The TypeScript SDK ships today. Python and Swift land when the first user asks. Until then, the OpenAPI spec at /openapi.json lets anyone generate a client in…
Internals, Runtime
How Amy's cloud actually executes on Cloudflare. One Worker (cloud/) ties together D1, R2, KV, Queues, and Cron. There are no Workflows yet, async work runs via a Queue consumer in the same Worker.…