# End-user server routes, Python and REST

## Contents
- Approval routes (TypeScript, Next.js App Router)
- Python (FastAPI style)
- REST endpoints
- Records and filters

## Approval routes (TypeScript)

Each route acts for the signed-in user and nobody else. An approval that belongs to another user, or one the team decides, answers `404`.

```typescript
// app/api/approvals/route.ts
import { anlyon } from '@/lib/anlyon';
import { auth } from '@/lib/auth';

export async function GET() {
  const session = await auth();
  if (!session) return new Response('Sign in.', { status: 401 });
  const { data } = await anlyon.endUsers.approvals.list(session.user.id, { status: 'pending' });
  return Response.json(data ?? []);
}
```

```typescript
// app/api/approvals/[id]/route.ts
import { anlyon } from '@/lib/anlyon';
import { auth } from '@/lib/auth';

export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const session = await auth();
  if (!session) return new Response('Sign in.', { status: 401 });
  const { id } = await params;
  const { decision } = (await request.json()) as { decision: 'approve' | 'deny' };
  if (decision !== 'approve' && decision !== 'deny') return new Response('Bad decision.', { status: 400 });

  const { data } = await anlyon.endUsers.approvals.decide(session.user.id, id, { decision });
  const effectId = data?.invocation?.effectId;
  const receipt = effectId ? (await anlyon.forUser(session.user.id).actions.effect(effectId)).data : null;
  return Response.json({
    status: data?.approval.status,
    receipt: receipt && { id: receipt.id, outcome: receipt.outcome, grade: receipt.grade },
  });
}
```

The approval's `payload.input` holds the exact input the call will send. Show it to the user in words they understand; the action's name and description are shown as written, so write them for the end user.

## Python

```python
import os
from anlyon import Client, EndUserConnectionRequiredError, parse_connect_redirect

anlyon = Client(api_key=os.environ["ANLYON_API_KEY"])

# Connect link
link = anlyon.end_users.create_connect_link(
    end_user_id=user.id, integration="github", redirect_url="https://app.example.com/connections/callback"
).data  # {"url", "expiresAt", "integration"}

# Callback: confirm for the signed-in user, never one read from the URL
result = parse_connect_redirect(str(request.url))
if result and result.status == "pending_confirmation" and result.flow_id:
    anlyon.end_users.confirm_connection(user.id, result.flow_id)

# Act for the user
try:
    anlyon.for_user(user.id).actions.invoke(
        "github-create-issue", {"owner": owner, "repo": repo, "title": title, "body": body}
    )
except EndUserConnectionRequiredError as error:
    connect_url = error.connect_url  # None when there is no default redirect URL

# The user's approvals
pending = anlyon.end_users.approvals.list(user.id, status="pending").data
anlyon.end_users.approvals.decide(user.id, approval_id, decision="approve")

# Session token for @anlyonhq/react
session = anlyon.end_users.create_session(user.id, ttl_seconds=600).data

# Removal
anlyon.end_users.delete_connection(user.id, "github")
anlyon.end_users.erase(user.id)
```

`client.for_user(id)` binds the user for good; the async client has the same methods.

## REST endpoints

Every call carries `Authorization: Bearer $ANLYON_API_KEY`. The SDKs also send `Anlyon-End-User` on these routes.

| Method and path | Scope | Does |
| --- | --- | --- |
| `POST /api/v2/end-users/{endUserId}/connect-links` `{ integration, redirectUrl? }` | `end-users:write` | Connect link (`201`, `{ url, expiresAt }`) |
| `POST /api/v2/end-users/{endUserId}/connections/confirm` `{ flowId }` | `end-users:write` | Confirm a flow for that user |
| `PUT /api/v2/end-users/{endUserId}/connections/{integration}` `{ token }` or `{ fields }` | `end-users:write` | Store the user's own key for an API-key integration (checked with the provider before it is stored) |
| `GET /api/v2/end-users/{endUserId}/connections` | `end-users:read` | The user's connections. Never a token |
| `DELETE /api/v2/end-users/{endUserId}/connections/{integration}` | `end-users:write` | Disconnect |
| `GET /api/v2/end-users/{endUserId}/approvals?status=pending` | `end-users:approvals` | The user's approvals |
| `POST /api/v2/end-users/{endUserId}/approvals/{approvalId}/decide` `{ decision, reason? }` | `end-users:approvals` | Decide; approving runs the parked call |
| `POST /api/v2/end-users/{endUserId}/sessions` `{ ttlSeconds? }` | `end-users:write` | Session token for the browser (minted with an API key) |
| `DELETE /api/v2/end-users/{endUserId}` | `end-users:write` | Erase the user |
| `POST /api/v2/actions/{ref}/invoke` with header `Anlyon-End-User: <id>` | `actions:invoke` | Invoke for that user |

## Records and filters

Invocations, receipts, approvals and audit records carry `endUserId`. `actions.invocations({ endUserId })` and the console filter by it. In the console, **Users** lists each user with connections, calls and pending approvals, and **Controls → Approvals → Waiting on your users** shows end-user approvals, which your team can see but not decide.
