Agent Endpoints
Expose custom HTTP API endpoints for your agent using the @http decorator. Endpoints are mounted under the agent's base path and are served by the same app used for chat completions.
- Simple, declarative decorator: TypeScript
@http({ path, method, scopes }), Python@http("/path", method="get|post", scope="..."). - Path parameters and query strings supported.
- Scope-based access control (
all,owner,admin). - Plays nicely with skills, tools, and hooks.
Basic Usage
import { BaseAgent, Skill, http, serve } from 'webagents';
class StatusSkill extends Skill {
readonly name = 'status';
@http({ path: '/status', method: 'GET' })
async getStatus(_req: Request): Promise<Response> {
return Response.json({ status: 'healthy' });
}
}
const agent = new BaseAgent({
name: 'assistant',
model: 'openai/gpt-4o-mini',
skills: [new StatusSkill()],
});
await serve(agent, { port: 8000 });Both expose GET /assistant/status.
Methods, Path, and Query
class UsersSkill extends Skill {
readonly name = 'users';
@http({ path: '/users', method: 'GET' })
async listUsers(_req: Request): Promise<Response> {
return Response.json({ users: ['alice', 'bob', 'charlie'] });
}
@http({ path: '/users', method: 'POST' })
async createUser(req: Request): Promise<Response> {
const data = await req.json();
return Response.json({ created: data.name, id: 'user_123' });
}
@http({ path: '/users/:userId', method: 'GET' })
async getUser(req: Request): Promise<Response> {
const url = new URL(req.url);
const userId = url.pathname.split('/').pop()!;
const includeDetails = url.searchParams.get('include_details') === 'true';
const user: Record<string, unknown> = { id: userId, name: `User ${userId}` };
if (includeDetails) user.details = 'Extended info';
return Response.json(user);
}
}Example requests:
# List users
curl http://localhost:8000/assistant/users
# Create user
curl -X POST http://localhost:8000/assistant/users \
-H "Content-Type: application/json" \
-d '{"name": "dana"}'
# Get user with query param
curl "http://localhost:8000/assistant/users/42?include_details=true"
# Missing or wrong Content-Type
curl -X POST http://localhost:8000/assistant/users -d '{"name":"dana"}'
# -> 415 Unsupported Media Type
# Wrong method
curl -X GET http://localhost:8000/assistant/users -H "Content-Type: application/json" -d '{}'
# -> 405 Method Not Allowed
# Unauthorized scope
curl http://localhost:8000/assistant/admin/metrics
# -> 403 ForbiddenCapability Discovery
Use provides (Python) / inferred capability (TypeScript via skill name) to declare what an endpoint provides:
@http({ path: '/export/pdf', method: 'POST', description: 'Export data as PDF' })
async exportPdf(req: Request): Promise<Response> {
const data = await req.json();
const pdf = await generatePdf(data);
return new Response(pdf, { headers: { 'content-type': 'application/pdf' } });
}
@http({ path: '/api/search', method: 'GET', description: 'Search API endpoint' })
async search(req: Request): Promise<Response> {
const query = new URL(req.url).searchParams.get('query') ?? '';
return Response.json({ results: await performSearch(query) });
}The provides value is included in the agent's capabilities for discovery.
Access Control (Scopes)
Use scopes (TypeScript) / scope (Python) to restrict who can call an endpoint:
@http({ path: '/public', method: 'GET', scopes: ['all'] })
async publicEndpoint(_req: Request): Promise<Response> {
return Response.json({ message: 'Public data' });
}
@http({ path: '/owner-info', method: 'GET', scopes: ['owner'] })
async ownerEndpoint(_req: Request): Promise<Response> {
return Response.json({ private: 'owner data' });
}
@http({ path: '/admin/metrics', method: 'GET', scopes: ['admin'] })
async adminMetrics(_req: Request): Promise<Response> {
return Response.json({ rps: 100, error_rate: 0.001 });
}WebSocket Endpoints
For bidirectional real-time communication, use the @websocket decorator:
import { Skill, websocket } from 'webagents';
import type { Context } from 'webagents';
class StreamSkill extends Skill {
readonly name = 'stream';
@websocket({ path: '/stream' })
handleStream(ws: WebSocket, ctx: Context): void {
ws.onmessage = async (ev) => {
const message = JSON.parse(String(ev.data));
const response = await this.process(message);
ws.send(JSON.stringify(response));
};
}
private async process(msg: unknown) { return { echo: msg }; }
}Both expose WS /assistant/stream.
WebSocket with LLM Streaming
Combine WebSocket with handoffs for streaming chat:
import { Skill, websocket } from 'webagents';
class StreamingSkill extends Skill {
readonly name = 'streaming';
@websocket({ path: '/chat' })
async handleChat(ws: WebSocket): Promise<void> {
ws.onmessage = async (ev) => {
const { messages = [] } = JSON.parse(String(ev.data));
for await (const chunk of this.executeHandoff(messages)) {
ws.send(JSON.stringify(chunk));
}
};
}
private async *executeHandoff(_: unknown[]) {
yield { delta: 'streaming chunk' };
}
}SSE Streaming (Server-Sent Events)
Return an async iterable from an @http handler to stream as SSE:
import { Skill, http } from 'webagents';
class EventsSkill extends Skill {
readonly name = 'events';
@http({ path: '/events', method: 'GET', content_type: 'text/event-stream' })
async streamEvents(_req: Request): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(encoder.encode(`data: {"count": ${i}}\n\n`));
await new Promise((r) => setTimeout(r, 1000));
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(stream, {
headers: {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive',
},
});
}
}The Python server automatically sets SSE headers (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive). In TypeScript, set them yourself on the Response.
Auto-Registration via Transport Skills
Transport skills register endpoints automatically when added to an agent — no manual endpoint wiring needed:
import { BaseAgent } from 'webagents';
import { CompletionsTransportSkill } from 'webagents/skills/transport/completions';
import { A2ATransportSkill } from 'webagents/skills/transport/a2a';
import { UAMPTransportSkill } from 'webagents/skills/transport/uamp';
const agent = new BaseAgent({
name: 'my-agent',
skills: [
new CompletionsTransportSkill(), // POST /v1/chat/completions, GET /v1/models
new A2ATransportSkill(), // POST /a2a, GET /.well-known/agent.json
new UAMPTransportSkill(), // WS /uamp
],
});Tips
- Keep one responsibility per endpoint (CRUD-style patterns work well).
- Prefer
GETfor retrieval,POSTfor creation/processing. - Validate inputs inside handlers; return JSON-serializable data.
- Register endpoints through skill classes alongside
@tool,@hook, and@handoff.
See Also
- Quickstart — serving agents
- Agent Skills — modular capabilities
- Tools — add executable functions
- Hooks — lifecycle integration