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

# Serve an API with owned sessions

> Run a local authenticated Agent API and verify missing credentials, session reuse, and user isolation.

Expose a support Agent through `@agentium/transport` and verify who can use a session. The default model is a local fixture. The example demonstrates the host hooks with two known demonstration credentials and binds only to `127.0.0.1`.

| You will use | Requirement |
| - | - |
| Packages | `@agentium/core`, `@agentium/transport`, `express`, `zod` |
| Default execution | No provider account; local HTTP server on port 3100 |
| Result | A JSON run response and a server-assigned session ID scoped to its owner |
| State | Process-local conversation history and ownership; lost on restart |

## Get the project

[Download the complete project](/downloads/authenticated-api.zip), extract it, and run `npm install`, `npm run check`, then `npm start`.

For manual setup in an empty directory:

```bash theme={null}
npm init -y
npm pkg set type=module
npm install @agentium/core@4.0.0 @agentium/transport@4.0.0 express zod
npm install --save-dev typescript tsx @types/node @types/express
```

## Create the service

The middleware recognizes the demo credentials. `resolveIdentity` receives only those host-verified claims. `authorizeResource` atomically creates an owner record for a new session and checks that record on reuse. Unrecognized resource operations are denied.

Save as `authenticated-api.ts`:

```typescript authenticated-api.ts theme={null}
import { pathToFileURL } from "node:url";
import express from "express";
import { Agent, openai, type ModelProvider } from "@agentium/core";
import { createAgentRouter } from "@agentium/transport";
import { z } from "zod";

const identitySchema = z.object({ userId: z.string(), tenantId: z.string() });
// Known demonstration credentials; bind this example only to loopback.
const users = new Map([
  ["Bearer docs-alice", { userId: "alice", tenantId: "demo" }],
  ["Bearer docs-bob", { userId: "bob", tenantId: "demo" }],
]);
const fixture: ModelProvider = {
  providerId: "fixture", modelId: "support",
  async generate() {
    return { message: { role: "assistant", content: "Please share your order ID." },
      finishReason: "stop", raw: null, usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 } };
  },
  async *stream() { throw new Error("Use the /run endpoint for this fixture"); },
};

export async function startSupportAPI(port = 3100, model: ModelProvider = fixture) {
  const support = new Agent({
    name: "support", model, register: false,
    instructions: "Help with orders. Ask for an order ID when one is missing.",
  });
  const owners = new Map<string, string>();
  const app = express();
  app.use(express.json({ limit: "16kb" }));
  app.use((req, _res, next) => {
    const identity = users.get(req.get("authorization") ?? "");
    if (identity) Object.assign(req, { user: identity });
    next();
  });
  app.use("/api", createAgentRouter({
    registry: false, agents: { support },
    security: {
      mode: "authenticated",
      resolveIdentity: (claims) => {
        const parsed = identitySchema.safeParse(claims);
        return parsed.success ? parsed.data : null;
      },
      authorizeResource: ({ identity, operation, resource }) => {
        if (resource.kind !== "session" || resource.agentName !== "support" || !resource.id) return false;
        const owner = JSON.stringify([identity.tenantId, identity.userId]);
        if (operation === "session:create") {
          if (owners.has(resource.id)) return false;
          owners.set(resource.id, owner); // Atomic within this one synchronous process.
          return true;
        }
        return operation === "session:use" && owners.get(resource.id) === owner;
      },
    },
  }));
  const server = app.listen(port, "127.0.0.1");
  try {
    await new Promise<void>((resolve, reject) => {
      server.once("listening", resolve);
      server.once("error", reject);
    });
  } catch (error) { await support.close(); throw error; }
  const address = server.address();
  if (!address || typeof address === "string") throw new Error("Expected a TCP listener");
  const url = `http://127.0.0.1:${address.port}/api/agents/support/run`;
  let closing: Promise<void> | undefined;
  function close() {
    return closing ??= new Promise<void>((resolve, reject) => {
      server.close((error) => error ? reject(error) : resolve());
    }).finally(() => support.close());
  }
  return { url, close };
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const live = process.argv.includes("--live");
  if (live && !process.env.OPENAI_API_KEY) throw new Error("Set OPENAI_API_KEY before using --live");
  const api = await startSupportAPI(3100, live ? openai(process.env.OPENAI_MODEL ?? "gpt-6-luna") : fixture);
  console.log("Support API:", api.url);
  const shutdown = () => { void api.close().catch((error) => { console.error(error); process.exitCode = 1; }); };
  process.once("SIGINT", shutdown);
  process.once("SIGTERM", shutdown);
}
```

```bash theme={null}
npx tsx authenticated-api.ts
```

Leave it running and use a second terminal for the requests below.

## Exercise the boundary

**1. Missing credentials.** Expect HTTP 401:

```bash theme={null}
curl -i http://127.0.0.1:3100/api/agents/support/run \
  -H 'Content-Type: application/json' \
  -d '{"input":"Where is my order?"}'
```

**2. Create Alice's session.** Expect HTTP 200 and `Please share your order ID.` in the response's `text`. Copy the `X-Agentium-Session-Id` response header:

```bash theme={null}
curl -i http://127.0.0.1:3100/api/agents/support/run \
  -H 'Authorization: Bearer docs-alice' \
  -H 'Content-Type: application/json' \
  -d '{"input":"Where is my order?"}'
```

**3. Reuse the owned session.** Replace the placeholder with that header value. Alice receives HTTP 200:

```bash theme={null}
export SESSION_ID="paste-the-returned-session-id"
curl -i http://127.0.0.1:3100/api/agents/support/run \
  -H 'Authorization: Bearer docs-alice' \
  -H 'Content-Type: application/json' \
  -d "{\"input\":\"My order is order-42\",\"sessionId\":\"$SESSION_ID\"}"
```

Change the authorization header to `Bearer docs-bob` with the same session ID. Expect HTTP 403. A request authenticated as Alice with `"userId":"bob"` in the body also fails with HTTP 403. The host does not trust a body field to select an identity.

The fixture always returns the same response. The Agent retains conversation history in its fallback in-memory session manager, so Alice’s second run receives the first turn. History and ownership records disappear on restart. Add [persistent session storage](/agents/sessions) when they must survive a process restart; long-term [memory](/memory/overview) is a separate choice.

## Connect and deploy your application

To try a real model explicitly:

```bash theme={null}
npm install openai
export OPENAI_API_KEY="your-key"
npx tsx authenticated-api.ts --live
```

Stop the previous process first to free port 3100. Replace the demo token map with your authentication middleware, replace `owners` with an authoritative store that binds session creation atomically, and apply application authorization to tools. Provider keys belong to the host.

The router serves core Agents, Teams, and Workflows. It does not automatically host a `HarnessRuntime`; a harness application needs a host adapter that maps verified requests into its runtime contract. See [harness runtime](/harness/runtime).

## Failures and shutdown

| Symptom | Next step |
| - | - |
| HTTP 401 | Supply recognized credentials; no user is inferred from the request body |
| Session access denied | Reuse the server-returned ID with its owner; arbitrary supplied IDs are not automatically registered |
| HTTP 403 for body identity | Remove conflicting `userId`/`tenantId`; the host determines identity |
| Address already in use | Stop the previous demo or change the `startSupportAPI()` port |
| Provider error in live mode | Check the provider key and model, then inspect run status rather than assuming every HTTP response is successful work |

Ctrl-C stops new connections, drains active requests, and closes the Agent. A production host should also enforce a bounded shutdown deadline. Continue with [streaming](/transport/express), [quality gates](/examples/quality-gate), and [hosted authentication](/ship/authentication).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.