# @atcute/client

> lightweight and cute API client for AT Protocol

Latest version **5.1.2** (published 2026-09-10) · 0BSD license · 0 weekly downloads

## Install

```sh
npm install @atcute/client
pnpm add @atcute/client
yarn add @atcute/client
bun add @atcute/client
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 5.1.2 |
| Published | 2026-09-10 |
| First published | 2024-08-18 |
| Weekly downloads | 0 |
| License | 0BSD |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 2 |
| Unpacked size | 42.3 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | externdefs |

## Links

- npm: https://www.npmjs.com/package/@atcute/client
- Repository: https://tangled.org/did:plc:pljn5qch4tgadongtc7i6qij
- npm.io page: https://npm.io/package/@atcute/client

## Dependencies (2)

- [@atcute/identity](https://npm.io/package/@atcute/identity.md) ^2.0.2
- [@atcute/lexicons](https://npm.io/package/@atcute/lexicons.md) ^2.1.0

## Recent versions

- 5.1.2 (latest) — 2026-09-10
- 5.1.1 — 2026-06-29
- 5.1.0 — 2026-06-12
- 5.0.0 — 2026-05-09
- 4.2.2 — 2026-05-08
- 4.2.1 — 2026-01-05
- 4.2.0 — 2025-12-30
- 4.1.2 — 2025-12-27
- 4.1.1 — 2025-12-10
- 4.1.0 — 2025-11-25
- 4.0.5 — 2025-10-14
- 4.0.4 — 2025-10-09
- 4.0.3 — 2025-05-27
- 4.0.2 — 2025-05-20
- 4.0.1 — 2025-05-17
- … 15 more at https://npm.io/package/@atcute/client/versions

## README

# @atcute/client

lightweight and cute API client for AT Protocol.

```sh
npm install @atcute/client @atcute/bluesky
```

## prerequisites

the client requires a definition package to know what queries and procedures are available. install
one alongside the client:

```sh
npm install @atcute/client @atcute/bluesky
```

then register the type definitions using one of these methods:

```jsonc
// tsconfig.json
{
	"compilerOptions": {
		"types": ["@atcute/bluesky"],
	},
}
```

```ts
// env.d.ts
/// <reference types="@atcute/bluesky" />
```

```ts
// or as an import in your entrypoint
import type {} from '@atcute/bluesky';
```

now the XRPC methods will have full type information for the registered schemas.

available packages:

| package                                                            | schemas                                 |
| ------------------------------------------------------------------ | --------------------------------------- |
| [`@atcute/atproto`](../../definitions/atproto)                     | `com.atproto.*`                         |
| [`@atcute/bluemoji`](../../definitions/bluemoji)                   | `blue.moji.*`                           |
| [`@atcute/bluesky`](../../definitions/bluesky)                     | `app.bsky.*`, `chat.bsky.*`             |
| [`@atcute/frontpage`](../../definitions/frontpage)                 | `fyi.unravel.frontpage.*`               |
| [`@atcute/germ`](../../definitions/germ)                           | `com.germnetwork.*`                     |
| [`@atcute/leaflet`](../../definitions/leaflet)                     | `pub.leaflet.*`                         |
| [`@atcute/lexicon-community`](../../definitions/lexicon-community) | `community.lexicon.*`                   |
| [`@atcute/microcosm`](../../definitions/microcosm)                 | `blue.microcosm.*`, `com.bad-example.*` |
| [`@atcute/ozone`](../../definitions/ozone)                         | `tools.ozone.*`                         |
| [`@atcute/pckt`](../../definitions/pckt)                           | `blog.pckt.*`                           |
| [`@atcute/standard-site`](../../definitions/standard-site)         | `site.standard.*`                       |
| [`@atcute/tangled`](../../definitions/tangled)                     | `sh.tangled.*`                          |
| [`@atcute/whitewind`](../../definitions/whitewind)                 | `com.whtwnd.*`                          |

you can register multiple packages to combine their types.

## usage

the client communicates with AT Protocol services using XRPC, a simple RPC framework over HTTP.
queries are GET requests, procedures are POST requests.

### making requests

```ts
import { Client, simpleFetchHandler } from '@atcute/client';
import type {} from '@atcute/bluesky';

// create a client pointing to the Bluesky public API
const rpc = new Client({ handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) });
```

use `get()` for queries and `post()` for procedures. both return a response object with `ok`,
`status`, `headers`, and `data` fields:

```ts
// queries use get()
const response = await rpc.get('app.bsky.actor.getProfile', {
	params: { actor: 'bsky.app' },
});

if (response.ok) {
	console.log(response.data.displayName);
	// -> "Bluesky"
}
```

```ts
// procedures use post()
const response = await rpc.post('com.atproto.repo.createRecord', {
	input: {
		repo: 'did:plc:1234...',
		collection: 'app.bsky.feed.post',
		record: {
			$type: 'app.bsky.feed.post',
			text: 'hello world!',
			createdAt: new Date().toISOString(),
		},
	},
});
```

### handling errors

responses always include an `ok` field indicating success. for failed requests, `data` contains an
error object with `error` (the error name) and optionally `message` (description):

```ts
const response = await rpc.get('app.bsky.actor.getProfile', {
	params: { actor: 'nonexistent.invalid' },
});

if (!response.ok) {
	console.log(response.data.error);
	// -> "InvalidRequest"
	console.log(response.data.message);
	// -> "Unable to resolve handle"
}
```

the error names are defined in the lexicon schema. you can switch on them for typed error handling:

```ts
if (!response.ok) {
	switch (response.data.error) {
		case 'InvalidRequest':
			// handle or account doesn't exist
			break;
		case 'AccountTakedown':
			// account was taken down
			break;
		case 'AccountDeactivated':
			// account deactivated by user
			break;
	}
}
```

### optimistic requests

if you prefer throwing on errors instead of checking `response.ok`, use the `ok()` helper:

```ts
import { Client, ok, simpleFetchHandler } from '@atcute/client';

const rpc = new Client({
	handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }),
});

// throws ClientResponseError if the request fails
const profile = await ok(rpc.get('app.bsky.actor.getProfile', { params: { actor: 'bsky.app' } }));

console.log(profile.displayName);
// -> "Bluesky"
```

catch errors with `ClientResponseError`:

```ts
import { ClientResponseError } from '@atcute/client';

try {
	const profile = await ok(rpc.get('app.bsky.actor.getProfile', { params: { actor: 'invalid' } }));
} catch (err) {
	if (err instanceof ClientResponseError) {
		console.log(err.error); // error name from server
		console.log(err.description); // error message from server
		console.log(err.status); // HTTP status code
	}
}
```

### authenticated requests

for password-based authentication, use `PasswordSession` from `@atcute/password-session`. it manages
tokens, automatically refreshes expired access tokens, and can persist sessions:

```sh
npm install @atcute/password-session
```

```ts
import { Client, ok } from '@atcute/client';
import { PasswordSession } from '@atcute/password-session';

const auth = await PasswordSession.login({
	service: 'https://bsky.social',
	identifier: 'you.bsky.social',
	password: 'your-app-password',
});

const rpc = new Client({ handler: auth });

// requests are now authenticated
const session = await ok(rpc.get('com.atproto.server.getSession'));
console.log(session.did);
// -> "did:plc:..."
```

save `auth.session` to persist login across app restarts:

```ts
// after login, save the session
localStorage.setItem('session', JSON.stringify(auth.session));
```

```ts
// later, restore the session
const saved = localStorage.getItem('session');
if (saved) {
	const auth = await PasswordSession.resume(JSON.parse(saved));
	const rpc = new Client({ handler: auth });
}
```

use callbacks to keep persisted sessions in sync:

```ts
const auth = await PasswordSession.login(
	{
		service: 'https://bsky.social',
		identifier: 'you.bsky.social',
		password: 'your-app-password',
	},
	{
		onUpdate(session) {
			// called on login and token refresh
			localStorage.setItem('session', JSON.stringify(session));
		},
		onDelete(session) {
			// called on logout or session invalidation
			localStorage.removeItem('session');
		},
	},
);
```

### response formats

by default, responses are parsed as JSON. for endpoints that return binary data, specify the format
with `as`:

```ts
// get response as a Blob
const { data: blob } = await ok(
	rpc.get('com.atproto.sync.getBlob', {
		params: { did: 'did:plc:...', cid: 'bafyrei...' },
		as: 'blob',
	}),
);

// get response as Uint8Array
const { data: bytes } = await ok(
	rpc.get('com.atproto.sync.getBlob', {
		params: { did: 'did:plc:...', cid: 'bafyrei...' },
		as: 'bytes',
	}),
);

// get response as ReadableStream
const { data: stream } = await ok(
	rpc.get('com.atproto.sync.getBlob', {
		params: { did: 'did:plc:...', cid: 'bafyrei...' },
		as: 'stream',
	}),
);

// discard response body
await ok(
	rpc.post('com.atproto.repo.deleteRecord', {
		input: { repo: 'did:plc:...', collection: '...', rkey: '...' },
		as: null,
	}),
);
```

### runtime validation

by default, responses are trusted without validation. for stricter guarantees, use `call()` with the
schema from a definition package:

```ts
import { Client, ok, simpleFetchHandler } from '@atcute/client';
import { AppBskyActorGetProfile } from '@atcute/bluesky';

const rpc = new Client({
	handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }),
});

// validates params, input, and output against the schema
const response = await rpc.call(AppBskyActorGetProfile, {
	params: { actor: 'bsky.app' },
});

if (response.ok) {
	// response.data is validated
	console.log(response.data.displayName);
}
```

validation errors throw `ClientValidationError`:

```ts
import { ClientValidationError } from '@atcute/client';

try {
	await rpc.call(AppBskyActorGetProfile, { params: { actor: 'invalid!' } });
} catch (err) {
	if (err instanceof ClientValidationError) {
		console.log(err.target); // 'params', 'input', or 'output'
		console.log(err.message); // validation error details
	}
}
```

### service proxying

service proxying lets you make authenticated requests through your PDS to other services. the PDS
forwards the request with authorization headers proving it's acting on your behalf.

```ts
import { Client, ok } from '@atcute/client';
import { PasswordSession } from '@atcute/password-session';

// must be authenticated
const session = await PasswordSession.login({
	service: 'https://bsky.social',
	identifier: 'you.bsky.social',
	password: 'your-app-password',
});

// create a client that proxies requests through your PDS to the chat service
const chatClient = new Client({
	handler: session,
	proxy: {
		did: 'did:web:api.bsky.chat',
		serviceId: '#bsky_chat',
	},
});

// request goes to your PDS, which forwards it to api.bsky.chat with auth headers
const convos = await ok(chatClient.get('chat.bsky.convo.listConvos'));
```

common service IDs include:

- `#atproto_pds` - personal data server
- `#atproto_labeler` - labeler service
- `#bsky_chat` - Bluesky chat service

### custom fetch handlers

the `simpleFetchHandler` works for most cases. for advanced scenarios, provide your own handler:

```ts
import type { FetchHandler } from '@atcute/client';

const customHandler: FetchHandler = async (pathname, init) => {
	// pathname is like "/xrpc/app.bsky.actor.getProfile?actor=bsky.app"
	const url = new URL(pathname, 'https://public.api.bsky.app');

	// add custom headers, logging, retry logic, etc.
	console.log(`${init.method?.toUpperCase()} ${url}`);

	return fetch(url, init);
};

const rpc = new Client({ handler: customHandler });
```

or implement `FetchHandlerObject` for stateful handlers (like `PasswordSession` does):

```ts
import type { FetchHandlerObject } from '@atcute/client';

class MyHandler implements FetchHandlerObject {
	async handle(pathname: string, init: RequestInit): Promise<Response> {
		// your implementation
		return fetch(new URL(pathname, 'https://...'), init);
	}
}

const rpc = new Client({ handler: new MyHandler() });
```

### rate limiting

`retryFetchHandler` wraps another handler to retry rate-limited (HTTP 429) responses. the delay
comes from the `Retry-After` or `RateLimit-Reset` header when present, otherwise exponential
backoff.

```ts
import { Client, retryFetchHandler, simpleFetchHandler } from '@atcute/client';

const rpc = new Client({
	handler: retryFetchHandler({
		handler: simpleFetchHandler({ service: 'https://bsky.social' }),
		maxRetries: 3, // attempts before giving up (default 3)
		maxDelay: 60_000, // upper bound on a retry delay, in ms (default 60s)
		fallbackDelay: 1_000, // backoff base when no header is present, or null to not retry (default 1s)
		shouldRetry: (response) => response.status === 429, // widen to retry other statuses
	}),
});
```

`parseRateLimitHeaders` reads the `RateLimit-*` headers off a response where they're exposed,
returning `null` when they're absent or malformed:

```ts
import { parseRateLimitHeaders } from '@atcute/client';

const response = await rpc.post('com.atproto.repo.createRecord', {/* ... */});

const info = parseRateLimitHeaders(response.headers);
if (info) {
	console.log(`${info.remaining}/${info.limit} remaining, resets at ${info.reset.toISOString()}`);
}
```

---
_Source: https://npm.io/package/@atcute/client · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
