# Low-level client (TypeScript)

> createRunstateClient, a thin promise-per-endpoint client for infrastructure authors, with no lease renewal or waiting helpers. TypeScript only.

Source: https://docs.getrunstate.com/sdk/low-level-client/

Most code should use the [`Runstate` client](https://docs.getrunstate.com/sdk/typescript/). It renews leases, waits for capacity, maps errors to typed classes and resolves names to ids. `createRunstateClient` does none of that: each method is one HTTP request. It exists for tools built directly on the API, where you want to control every request, including its `Idempotency-Key`.

> **Note**
>
> There is no Python equivalent. From Python, use `AsyncRunstate` or call the [HTTP API](https://docs.getrunstate.com/api/) directly.

```ts
import { createRunstateClient, KernelError } from 'runstate-sdk';

const client = createRunstateClient({
  baseUrl: process.env.RUNSTATE_BASE_URL ?? 'https://api.getrunstate.com',
  apiKey: process.env.RUNSTATE_API_KEY!,
});
const space = client.space(process.env.RUNSTATE_SPACE_ID!);

const run = await space.createScope({ idempotencyKey: `nightly-${new Date().toISOString().slice(0, 10)}` });
const mailbox = await space.createMailbox({ name: 'crawl', idempotencyKey: 'create-crawl-queue' });
await space.mailbox(mailbox.id).send({
  scopeId: run.id,
  payload: { url: 'https://example.com' },
  idempotencyKey: 'crawl-example.com',
});

const msg = await space.mailbox(mailbox.id).recv<{ url: string }>({
  scopeId: run.id,
  holder: 'crawler-1',
  session: 'crawler-1-boot-42',
  leaseSeconds: 30,
});
if (msg !== null) {
  try {
    await msg.renew(60); // nothing renews for you
    await msg.complete();
  } catch (err) {
    if (err instanceof KernelError) console.error(err.code, err.httpStatus);
    throw err;
  }
}
```

## Differences from `Runstate`

- **You pass identity explicitly.** `holder` and `session` go on every claim, receive and acquire. Use a fresh `session` per process.
- **Ids, not names.** Methods take mailbox, permit and barrier ids.
- **No renewal, no waiting.** Leases expire unless you call `renew()`. Busy and exhausted responses are thrown immediately.
- **Idempotency keys are yours.** Pass `idempotencyKey` on the create and send methods that accept it. The client doesn't generate one, and the API rejects creates and sends without one (`VALIDATION_FAILED`).
- **Errors are `KernelError`** (with `code` and `httpStatus`), not `RunstateError` subclasses. Codes the client doesn't recognize become `UNAVAILABLE`.
- **Retries.** Transport failures are retried up to 3 times with 25–100 ms jitter; error responses are not retried.
- **Smaller surface.** It covers runs, claims, mailboxes and messages, events, timers, permits and grants, and barriers. Tasks, task groups, quotas, work limits and budgets are only in `Runstate` and the HTTP API.

## Methods

```ts
function createRunstateClient(options: { baseUrl: string; apiKey: string; fetchImpl?: typeof fetch }): RunstateClient;

interface RunstateClient {
  space(spaceId: string): SpaceClient;
}

interface SpaceClient {
  createScope(opts: { parentId?: string; deadline?: string; childLimit?: number; idempotencyKey?: string }): Promise<{ id: string; state: string; version: string }>;
  scope(scopeId: string): {
    get(): Promise<{ id: string; storedState: string; effective: string }>;
    cancel(): Promise<{ id: string; state: string; version: string }>;
    complete(): Promise<{ id: string; state: string; version: string }>;
  };

  claim(key: string, opts: { scopeId: string; holder: string; session: string; leaseSeconds?: number; idempotencyKey?: string }): Promise<ClaimHandle>;

  createMailbox(opts: { name: string; mode?: 'WORK' | 'INBOX'; backlogLimit?: number; idempotencyKey?: string }): Promise<{ id: string; name: string; mode: string }>;
  mailbox(mailboxId: string): {
    send(opts: { scopeId: string; payload: unknown; workKey?: string; deadline?: string; idempotencyKey?: string }): Promise<{ id: string; replay: boolean }>;
    recv<T = unknown>(opts: { scopeId: string; holder: string; session: string; leaseSeconds?: number }): Promise<MessageHandle<T> | null>;
  };

  events(opts?: { cursor?: string; limit?: number }): Promise<EventPage>;

  armTimer(opts: { scopeId: string; at?: string; afterSeconds?: number; idempotencyKey?: string }): Promise<{ id: string; fireAt: string }>;
  pollTimers(scopeId?: string): Promise<FiredTimer[]>;
  ackTimer(timerId: string, token: string): Promise<{ state: string }>;
  cancelTimer(timerId: string): Promise<{ state: string }>;

  createPermit(opts: { name: string; units: number; idempotencyKey?: string }): Promise<{ id: string; name: string; unitsTotal: number }>;
  acquirePermit(permitId: string, opts: { scopeId: string; units: number; holder: string; session: string; leaseSeconds?: number; waiterId?: string }): Promise<GrantAcquired>;
  waitPermit(permitId: string, opts: { scopeId: string; units: number; holder: string }): Promise<{ waiterId: string }>;
  releaseGrant(grantId: string, token: string, observation?: unknown): Promise<{ state: string }>;

  createBarrier(opts: { scopeId: string; target: number; idempotencyKey?: string }): Promise<{ id: string }>;
  arriveBarrier(barrierId: string, arrivalKey: string): Promise<{ state: string; arrivals: number }>;
  barrier(barrierId: string): Promise<BarrierView>;
}

interface ClaimHandle {
  key: string;
  generation: string;
  leaseExpiresAt: string;
  token: string;
  renew(leaseSeconds?: number): Promise<{ leaseExpiresAt: string }>;
  release(observation?: unknown): Promise<{ state: string }>;
}

interface MessageHandle<T = unknown> {
  message: { id: string; scopeId: string; payload: T; attempt: number; deadline: string | null };
  token: string;
  generation: string;
  leaseExpiresAt: string;
  renew(leaseSeconds?: number): Promise<{ leaseExpiresAt: string }>;
  nack(retry: boolean): Promise<{ state: string }>;
  complete(result?: { mailboxId: string; scopeId: string; payload: unknown; workKey?: string }): Promise<{ state: string; resultMessageId?: string }>;
}

interface GrantAcquired { grantId: string; token: string; leaseExpiresAt: string }
interface FiredTimer { id: string; token: string; firedAt: string; scopeId: string }
interface BarrierView { id: string; state: string; epoch: string; arrivals: number; target: number }
```

The low-level `complete()` type requires `mailboxId` and `scopeId` when you pass a result. To record a task outcome without publishing a result message, use `delivery.complete({ payload })` from the `Runstate` client or the HTTP API.
