> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usefused.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom HTTP endpoints

> Add application-specific FastAPI endpoints that invoke your Harnest agent.

Add custom endpoints when your application needs a business-specific HTTP contract in addition to Harnest's neutral API. The same extension works with managed or advanced ADK and LangGraph agents.

## Add an endpoint

Create a synchronous route factory in the root agent's `extensions/` directory. Harnest injects an `AgentInvoker` and mounts the returned FastAPI router.

```python extensions/http.py theme={null}
from fastapi import APIRouter, Request
from harnest import AgentInvoker, lifecycle


@lifecycle.http_routes
def http_routes(agent: AgentInvoker):
    router = APIRouter(prefix="/threadify")

    @router.post("/execute")
    async def execute(payload: dict, request: Request):
        response = await agent.invoke(
            connection=request,
            input=payload["message"],
            session_id=payload.get("sessionId"),
            metadata={"source": "threadify"},
        )
        return {
            "sessionId": response.session_id,
            "status": response.status,
            "answer": response.output_text,
            "result": response.result,
            "requiredAction": response.required_action,
        }

    return router
```

The factory can return multiple routes or use FastAPI dependencies. Define route factories only at the root. SubAgents do not own server paths.

## Invoke the agent safely

`AgentInvoker` uses the same response coordinator as `POST /responses`. It does not call the raw ADK or LangGraph object.

| Behavior                  | Custom endpoint result                                                           |
| ------------------------- | -------------------------------------------------------------------------------- |
| Authentication            | Harnest derives identity from `connection`; callers cannot supply `user_id`      |
| Session omitted           | Harnest creates a user-scoped session                                            |
| Session supplied          | Harnest verifies that the authenticated caller owns it                           |
| Input schema              | Harnest validates the configured Pydantic input model                            |
| Lifecycle and credentials | The normal invocation bindings run                                               |
| Approval or client tool   | `status` is `requires_action` and `required_action` describes the continuation   |
| Telemetry and limits      | Standard tracing, logging, timeout, concurrency, and request-size policy applies |

<Warning>
  Pass the current FastAPI `Request` as `connection`. Do not accept or construct a `user_id` from the request body.
</Warning>

## Handle required actions

An invocation can pause instead of returning a final answer:

```python theme={null}
response = await agent.invoke(connection=request, input=payload["message"])

if response.status == "requires_action":
    return {
        "status": response.status,
        "requiredAction": response.required_action,
    }
```

Resume human approvals through `POST /approvals/{approvalId}`. Submit browser-hosted tool results through `POST /client-tools/{requestId}`. These endpoints share the same suspended execution created by the custom route.

Use `response.as_dict()` when your endpoint should return Harnest's complete neutral response shape unchanged.

## Add an application-specific session view

Keep business dimensions such as site origin, workspace, inbox, or project out
of the neutral `/sessions` contract. Add an authenticated custom endpoint and
query an application-owned index instead:

```python extensions/http.py theme={null}
from fastapi import APIRouter, Request
from harnest import AgentInvoker, lifecycle
from harnest.runtime_auth import principal_for
from harnest.lib.site_sessions import site_sessions


@lifecycle.http_routes
def site_session_routes(_agent: AgentInvoker):
    router = APIRouter(prefix="/sites")

    @router.get("/{origin}/sessions")
    async def sessions(
        origin: str,
        request: Request,
        cursor: str | None = None,
        limit: int = 50,
    ):
        principal = principal_for(request)
        return await site_sessions.list(
            user_id=principal.user_id,
            origin=origin,
            cursor=cursor,
            limit=min(max(limit, 1), 100),
        )

    return router
```

`site_sessions` represents your indexed projection or repository, not a
Harnest type. Update it when a session is associated with an origin and scope
its index by the authenticated `user_id`. This provides complete server-side
filtering and global ordering without teaching Harnest about one application's
domain model.

<Warning>
  Do not fetch one bounded `/sessions` page and filter it locally. Matching
  sessions may exist on later pages, and sorting a page is not the same as
  sorting the complete filtered result.
</Warning>

## Route ownership

Compilation rejects duplicate routes and Harnest-owned namespaces. Reserved paths include `/responses`, `/sessions`, `/live`, `/approvals`, `/client-tools`, `/agent`, `/healthz`, playground assets, OpenAPI pages, and ADK-native run or application routes.

When you configure authentication, custom routes are protected by default. They also appear in `/openapi.json`. Harnest does not currently provide streaming through `AgentInvoker`; use the neutral SSE or WebSocket APIs when you need streaming.

See [Neutral API](/harnest/runtime/serving/neutral-api), [Approvals and client tools](/harnest/runtime/serving/approvals-and-client-tools), and [Authentication and credentials](/harnest/runtime/authentication-and-credentials).
