---
title: "Components (beta)"
description: "Prebuilt React components for your end users: approve what your agent wants to do, and connect their own accounts, inside your app. A short-lived session token keeps your API key on your server."
---

`@anlyonhq/react` gives you the screens [Act for your users](/guides/act-for-your-users) otherwise has you build by hand: an approval inbox, an inline approval, connect buttons and a connected-accounts list. They follow your shadcn/ui theme, or your own CSS variables.

This package is a beta release. Its API may change before 1.0.

```bash
npm install @anlyonhq/react @anlyonhq/sdk
```

React 18 or 19. No runtime dependencies beyond React.

## How it fits together

- **Your server mints a session token** for the user who is signed in, with your API key. The key never reaches the browser.
- **The provider fetches the token** from your route, keeps it in memory, and fetches a new one before it expires.
- **The components call Anlyon from the browser** with that token. It acts for that one user, in one environment.

A session token can:

- list the user's connected accounts,
- create a connect link (it returns only to one of your allowed redirect URLs),
- disconnect one of the user's accounts,
- list the user's own approvals, and approve or decline them, when your key has `end-users:approvals`.

Nothing else. It cannot call actions, read secrets, see other users, or confirm a connection. It lasts 15 minutes by default, at most 1 hour, and never past the key that minted it. Revoking that key ends its sessions. Each one is recorded in the audit log as `end_user.session_created`.

## 1. Set up the environment

In the console, open **Integrations → Connect settings** and pick the environment. Add your callback URL to the **allowed redirect URLs**, for example `https://app.example.com/api/anlyon/connected`.

In a browser, a session token works only from the origins of these URLs. A request from any other origin is refused with `403 ORIGIN_NOT_ALLOWED`, so another website cannot use a token it gets hold of. Outside a browser there is no origin to check: like any bearer token, a copied token works until it expires, for that one user and only for the operations above. Keep the lifetime short. For local work, add your local callback, such as `http://localhost:5173/api/anlyon/connected`, to a separate environment.

Create an API key with `end-users:write`, and `end-users:approvals` for the approval components.

## 2. The session route

```typescript
// app/api/anlyon/session/route.ts
import { Client } from '@anlyonhq/sdk';
import { createSessionHandler } from '@anlyonhq/react/server';
import { auth } from '@/lib/auth';

const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });

export const POST = createSessionHandler({
  client: anlyon,
  getEndUserId: async () => (await auth())?.user.id,
});
```

It answers `{ token, expiresAt }` with `Cache-Control: no-store`, and `401` when nobody is signed in. The user always comes from your own session, never from the request.

Without the helper, call [`endUsers.createSession`](/api-reference) yourself:

```typescript
const { data } = await anlyon.endUsers.createSession(session.user.id, { ttlSeconds: 600 });
return Response.json({ token: data!.token, expiresAt: data!.expiresAt }, { headers: { 'Cache-Control': 'no-store' } });
```

```python
session = client.end_users.create_session(user.id, ttl_seconds=600)
```

## 3. The connect callback

When a user connects an account, the provider sends them back to your callback URL. Your server confirms the connection for the user who is signed in, so a forwarded link cannot attach someone else's account. The helper does that, then closes the popup or redirects.

```typescript
// app/api/anlyon/connected/route.ts
import { createConnectCallbackHandler } from '@anlyonhq/react/server';

export const GET = createConnectCallbackHandler({
  client: anlyon,
  getEndUserId: async () => (await auth())?.user.id,
  redirectTo: '/settings/connections',
});
```

## 4. The provider

```tsx
// app/providers.tsx
'use client';
import { AnlyonProvider } from '@anlyonhq/react';
import '@anlyonhq/react/styles.css';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AnlyonProvider
      getToken={() => fetch('/api/anlyon/session', { method: 'POST' }).then((r) => r.json())}
      baseUrl={process.env.NEXT_PUBLIC_ANLYON_BASE_URL} // unset: https://api.anlyon.com
      connect={{ redirectUrl: 'https://app.example.com/api/anlyon/connected' }}
    >
      {children}
    </AnlyonProvider>
  );
}
```

| Prop | What it does |
|---|---|
| `getToken` | Returns `{ token, expiresAt }`, or the token. Called again a minute before expiry, and after the API refuses a token. |
| `baseUrl` | Defaults to `https://api.anlyon.com`. Set it for another environment's API host. |
| `connect` | `{ mode, redirectUrl }`. `mode` is `popup` (default) or `redirect`. A blocked popup falls back to a redirect. |
| `pollInterval` | How often open approvals are checked while the page is visible, in ms. Default `30000`. `0` turns it off. |
| `locale` | For amounts and dates. Defaults to the browser's. |

The provider holds one user's session. When the signed-in user changes without a page load, remount it with that user's id as its `key`, so nothing from the previous user stays on screen.

## 5. The components

### Approval inbox

```tsx
import { ApprovalInbox } from '@anlyonhq/react';

<ApprovalInbox />
```

Every approval waiting for this user, newest first. Each row opens to what the agent wants: the action in words, its input, the preview (fields, a diff, the amount), and when it expires. The user approves or declines, with an optional note. Pass `status="all"` to include answered and expired ones.

The user sees each action's name and description as you wrote them, so write them for that person, not for your team.

Approvals reach the user when a policy is **Decided by: End user**. See [Act for your users](/guides/act-for-your-users#4-add-the-limit-and-the-approval).

### One approval

```tsx
import { ApprovalPrompt } from '@anlyonhq/react';

<ApprovalPrompt approvalId={toolResult.approvalId} onDecided={(decision) => console.log(decision)} />
```

Use it inline, for example under the chat message whose tool call is waiting.

Both show each state as it is: waiting, sending, approved (with the action's outcome), declined, expired, and failed with the API's own message. If someone decided first, the component shows what actually happened.

### Connect button

```tsx
import { ConnectButton } from '@anlyonhq/react';

<ConnectButton integration="github" />
```

Opens the connect link in a popup. It shows **Reconnect GitHub** when the connection stopped working, and **GitHub connected** once it works.

### Connected accounts

```tsx
import { Connections } from '@anlyonhq/react';

<Connections integrations={['github', 'slack', 'notion']} />
```

The user's accounts with their status. Reconnect where needed. Disconnect asks first. `integrations` offers ones the user has not connected yet.

### Connect prompt for AI SDK tool results

```tsx
import { ConnectPrompt } from '@anlyonhq/react';

{message.parts.map((part) =>
  part.type.startsWith('tool-') && part.state === 'output-available' ? (
    <ConnectPrompt key={part.toolCallId} result={part.output} />
  ) : null,
)}
```

When an `@anlyonhq/ai-sdk` tool returns `connect_required` or `reconnect_required`, it renders a connect card. For any other result it renders nothing.

It takes `mode` and `redirectUrl`, which override the provider's `connect`, `onConnected(integration)`, called once the account is connected in popup mode (send the user's message again from there), and `name`, the integration's name in its labels.

### Hooks

Each component is built on a hook, for your own markup: `useApprovals`, `useApproval`, `useConnections` and `useConnect`. Errors are `AnlyonUIError`, with the API's `status`, `code` and `message`.

## Theming

### With shadcn/ui

Nothing to do. Each color reads the shadcn token of the same role (`--background`, `--foreground`, `--primary`, `--muted`, `--border`, `--ring`, `--destructive`, `--radius`), so the components follow your theme, light and dark. Tokens in OKLCH, hex or `rgb()` work as they are. If your theme stores bare HSL channels (`--background: 0 0% 100%`), add `class="anlyon-shadcn-hsl"` to `<html>`.

### With CSS variables

Set `--anlyon-*` variables. They win over shadcn tokens.

```css
:root {
  --anlyon-primary: #4f46e5;
  --anlyon-primary-fg: #ffffff;
  --anlyon-radius: 10px;
}
```

The variables are `--anlyon-bg`, `-fg`, `-muted`, `-muted-fg`, `-accent`, `-border`, `-primary`, `-primary-fg`, `-destructive`, `-destructive-fg`, `-ring`, `-radius`, `-success` and `-warning`.

Dark mode follows the system setting. A `.dark` or `[data-theme="dark"]` ancestor forces dark, and `.light` or `[data-theme="light"]` forces light.

### Your own styles

Every component takes `className`. The bundled rules weigh no more than one class, so yours win. Pass `unstyled` to drop the bundled styles entirely; the `anlyon-*` class names and `data-state` attributes stay for your CSS. The package README lists them.

## What stays on your server

- Your API key, and minting session tokens.
- Confirming a connection.
- Calling actions, and everything else a session token cannot do.

If a session token leaks, it works for one user, until it expires, and only for the operations above. Browsers on other websites cannot use it; a script outside a browser can. Revoking the key that minted it ends it at once, and so does erasing that user.

## Errors

| Code | Status | Meaning |
|---|---|---|
| `END_USER_SESSION_REQUIRED` | 401 | No token was sent. |
| `END_USER_SESSION_EXPIRED` | 401 | The token expired. The provider fetches a new one and retries once. |
| `END_USER_SESSION_INVALID` | 401 | The token was altered, or its key was revoked. |
| `END_USER_SESSION_SCOPE` | 403 | The key that minted it lacks `end-users:approvals`. |
| `ORIGIN_NOT_ALLOWED` | 403 | The page's origin is not one of the allowed redirect URLs' origins. |
| `END_USER_SESSION_NOT_ALLOWED` | 403 | A session token was sent to any other route. |
| `END_USER_SESSION_API_KEY_REQUIRED` | 403 | Only an API key can mint a session. |
