# SDK

> A typed TypeScript client for the Cymo API: one method per operation, async iterators for lists and streams, and webhook verification.

## Install

```sh
npm install @cymos/sdk
```

`@cymos/sdk` is an ES module for Node.js 20.19 or later. Keep it on your server: the key it holds can read your rooms.

## Create a client

Create a key in the dashboard and pass it as `apiKey`. A test key reads the simulator and a live key reads your paired Cymos; the code is the same.

`cymo.ts`

```ts
import { Cymo } from "@cymos/sdk";

const cymo = new Cymo({
  apiKey: process.env.CYMO_API_KEY!, // cym_test_… or cym_live_…
  timeoutMs: 10_000,
  maxRetries: 2,
  client: "my-app/1.2.0",
});
```

- `apiKey` · string, required — A cym\_test\_… or cym\_live\_… key.
- `baseUrl` · default https://api.cymatics.tech — An HTTPS origin, or HTTP on localhost for a local simulator.
- `mode` · "test" | "live" — Inferred from the key's prefix. A response from the other mode throws mode\_mismatch.
- `timeoutMs` · default 10000 — Per request, from 1 ms to one hour.
- `maxRetries` · default 2 — Automatic retries, from 0 to 5\. See Errors and retries below.
- `client` · default sdk/<version> — Sent as X-Cymo-Client, so request logs show which app made each call.
- `fetch` · typeof fetch — A custom fetch, for tests or proxies.

## Quickstart

`agent.ts`

```ts
import { Cymo } from "@cymos/sdk";

const cymo = new Cymo({ apiKey: process.env.CYMO_API_KEY! });

const { data, meta } = await cymo.vitals.get({ room_id: "room_bedroom" });
for (const person of data.people) {
  console.log(person.track_id, person.signals.heart_rate_bpm?.value);
}
console.log(meta.livemode); // false: test keys read the simulator
```

## Resource methods

Every API operation is a method named after its resource. Path parameters, query parameters and the request body go in one object, checked against the contract before anything is sent, so a typo throws `invalid_arguments` instead of reaching the API. Each [API reference](https://docs.cymatics.tech/api) page shows its method.

`TypeScript`

```ts
const { data, meta } = await cymo.scenarioRuns.create({
  scenario_id: "bed_exit",
  speed: 1,
  seed: 42
});
```

- `cymo.rooms.get()` — [GET /v1/rooms/{room}](https://docs.cymatics.tech/api/get-room)
- `cymo.vitals.get()` — [GET /v1/vitals](https://docs.cymatics.tech/api/get-vitals)
- `cymo.results.list()` · also cymo.results.listAll() — [GET /v1/results](https://docs.cymatics.tech/api/list-results)
- `cymo.events.list()` · also cymo.events.listAll() — [GET /v1/events](https://docs.cymatics.tech/api/list-events)
- `cymo.rooms.stream()` — [GET /v1/rooms/{room}/stream](https://docs.cymatics.tech/api/stream-room)
- `cymo.webhooks.create()` — [POST /v1/webhooks](https://docs.cymatics.tech/api/create-webhook)
- `cymo.scenarioRuns.create()` — [POST /v1/scenario-runs](https://docs.cymatics.tech/api/create-scenario-run)
- `cymo.scenarioRuns.control()` — [POST /v1/scenario-runs/{run}/control](https://docs.cymatics.tech/api/control-scenario-run)
- `cymo.rawData.exports.download()` — [GET /v1/raw-data/exports/{export}/content](https://docs.cymatics.tech/api/download-raw-data-export)
- `cymo.usage.get()` — [GET /v1/usage](https://docs.cymatics.tech/api/get-usage)

## Responses

Methods resolve to the API's envelope unchanged: `{ data, meta }`, and lists add `page`. Field names match the REST API exactly, in `snake_case`.

`TypeScript`

```ts
const { data, meta } = await cymo.rooms.get({ room_id: "room_bedroom" });

data.people;     // RoomSnapshot, typed from the contract
meta.request_id; // quote it to support
meta.livemode;   // false for test keys
```

## Pagination

`list` returns one page. `listAll` is an async iterator over every item: it requests the next page only when you reach it, stops when you `break`, and throws `invalid_response` if a cursor fails to advance.

`TypeScript`

```ts
// One page: data, plus page.has_more and page.next_cursor
const { data, page } = await cymo.events.list({ room_id: "room_bedroom", limit: 100 });

// Every page, requested as you iterate
for await (const event of cymo.events.listAll({ room_id: "room_bedroom" })) {
  console.log(event.type, event.timestamp);
}
```

## Streams

`rooms.stream`, `people.stream` and `vitals.stream` return async iterators of `{ data, meta, id, kind }`. After a dropped connection the SDK reconnects with `Last-Event-ID`, up to `maxReconnects` times (default 2), so no message is lost; API errors are thrown straight away. Pass `lastEventId` to resume a stream you closed, and an `AbortSignal` to stop one.

`TypeScript`

```ts
let lastEventId: string | undefined;
const controller = new AbortController();

for await (const message of cymo.rooms.stream(
  { room_id: "room_bedroom" },
  { signal: controller.signal, maxReconnects: 5 },
)) {
  lastEventId = message.id;
  console.log(message.kind, message.data);
}

// Later: pick up after the last message you handled
cymo.rooms.stream({ room_id: "room_bedroom" }, { lastEventId });

// Heart rate and respiration only, from the same room stream
cymo.vitals.stream({ room_id: "room_bedroom" });
```

Streams also take `reconnectDelayMs`, `idleTimeoutMs` (default 30 s without a byte, then `stream_idle_timeout`) and `maxMessageBytes` (default 1 MiB). See [Streaming](https://docs.cymatics.tech/streaming) for the wire format.

## Binary downloads

Raw-data downloads and raw device streams yield `Uint8Array` chunks. `maxBytes` stops after that many bytes, and the SDK checks the response belongs to your key's mode before yielding any.

`TypeScript`

```ts
for await (const chunk of cymo.rawData.exports.download({
  export_id: "exp_01"
}, { maxBytes: 1_048_576 })) {
  process.stdout.write(chunk);
}
```

## Errors and retries

Every failure is a `CymoError` with the API's stable `code`, or one of the SDK's own, such as `timeout`, `network_error`, `cancelled` or `redirect_refused`. The key is never sent through a redirect.

`TypeScript`

```ts
import { CymoError } from "@cymos/sdk";

try {
  await cymo.rooms.get({ room_id: "room_missing" });
} catch (error) {
  if (!(error instanceof CymoError)) throw error;
  error.code;           // "resource_not_found"
  error.status;         // 404
  error.request_id;     // "req_…"
  error.retry_after_ms; // from Retry-After, when sent
  error.rate_limit;     // { limit, remaining, reset_ms }
  error.details;        // invalid fields, for example
}
```

Within `maxRetries`, the SDK retries `rate_limit_exceeded` on any request after `Retry-After`, up to 30 seconds. Reads and requests with an idempotency key also retry network failures and `502`, `503` and `504`. It never retries `quota_exceeded` or `live_unavailable`. See [Errors](https://docs.cymatics.tech/errors) for every code.

## Idempotency

Every method takes a second argument for `signal`, `timeoutMs`, `maxRetries` and `idempotencyKey`. With a key, a write is safe to retry: the SDK retries it like a read, and the API replays the first response instead of acting twice.

`TypeScript`

```ts
await cymo.webhooks.create(
  { url: "https://example.com/api/cymo-webhook", events: ["bed.exited"] },
  { idempotencyKey: crypto.randomUUID() },
);
```

## Verify webhooks

`verifyWebhookSignature` checks a delivery's HMAC-SHA256 signature in constant time and rejects a signing time outside the tolerance.

`TypeScript`

```ts
import { verifyWebhookSignature } from "@cymos/sdk";

// Resolves when valid; otherwise throws CymoError with code
// invalid_webhook_signature or webhook_timestamp_out_of_tolerance.
await verifyWebhookSignature({
  payload,      // string | Uint8Array: the raw body exactly as received
  header,       // the Cymo-Signature header
  deliveryId,   // the Cymo-Delivery-Id header
  secret,       // string | string[]: any one may match, while you rotate
  toleranceSec, // optional, default 300
  now,          // optional, epoch milliseconds, default Date.now()
});
```

[Integrations](https://docs.cymatics.tech/integrations#webhooks) has a complete Next.js route handler, and [Events and webhooks](https://docs.cymatics.tech/webhooks#signatures) explains the scheme.

## TypeScript types

Arguments, responses and stream messages are typed from the same contract as the API, including domain types such as `RoomSnapshot`, `Result`, `Event` and `ScenarioRun`. Index `OperationArgs` and `OperationResponses` by operation ID to type your own wrappers.

`TypeScript`

```ts
import type {
  Envelope,
  ListEnvelope,
  StreamMessage,
  RoomSnapshot,
  Event,
  OperationArgs,
  OperationResponses,
} from "@cymos/sdk";

type CreateRunArgs = OperationArgs["create-scenario-run"];
type Room = OperationResponses["get-room"]; // Envelope<RoomSnapshot>
```
