> For the complete documentation index, see [llms.txt](https://docs.ojin.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ojin.ai/apps/overview/api-reference.md).

# Session API Reference

Start agent sessions programmatically. The widget uses this API automatically, call it directly for custom integrations.

## Base URL

```
https://api.ojin.ai
```

There is one endpoint. It is not regional, so there is no region to choose. Every path below already includes the `/v1` prefix, so append it to the base URL as written.

{% hint style="info" %}
**This is not the same endpoint as the one the "deploy in US East" guidance refers to.** They are two different things:

* **`api.ojin.ai` (this API)** is the agent control plane. You call it to start and cancel sessions. One global endpoint, no region in the hostname.
* **The models WebSocket** is where audio and video stream. If you drive the face models yourself with the [Python SDK](/models/build-with-python-sdk.md) or the [realtime API](/models/introduction/api.md), run **your own backend** in US East, close to Ojin's inference. See [Optimizing Performance](/guides/optimizing-performance.md).

If you only use the Human Agent widget or this Session API, the US East guidance does not apply to you.
{% endhint %}

## Start a Session

### `POST /v1/public/agents/connect`

Start a new session for a given agent. Returns connection details for joining the WebRTC room.

### Request

**Headers:**

| Header                 | Required                         | Description                                                                                                                                |
| ---------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `Content-Type`         | Yes                              | `application/json`                                                                                                                         |
| `Origin`               | Yes, unless the agent allows `*` | The browser sets this. It is checked against the agent's allowed hostnames (see [Authentication](#authentication))                         |
| `X-Session-Attempt-Id` | No                               | A client-generated id for this attempt. Lets the server correlate retries, and is what makes a `409 session_already_in_flight` recoverable |

**Body:**

```json
{
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "client_user_ref": "optional-user-identifier"
}
```

| Field             | Type          | Required | Description                                                                                                                        |
| ----------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`        | string (UUID) | Yes      | The ID of the agent to start a session with. Must be a UUID: any other string is rejected with `400` before the agent is looked up |
| `client_user_ref` | string        | No       | An opaque identifier for your end user. Appears in call history for analytics and tracking. Max 255 characters.                    |

Unknown body properties are rejected, not ignored: sending a field that is not listed here returns `400`.

### Response (200 OK)

```json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "agent_id": "3f1c2d4e-5a6b-47c8-9d0e-1f2a3b4c5d6e",
  "room_url": "https://example.daily.co/room-name",
  "room_name": "room-name",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "provider": "daily"
}
```

| Field        | Type          | Description                                                                                       |
| ------------ | ------------- | ------------------------------------------------------------------------------------------------- |
| `session_id` | string        | Unique identifier for this session                                                                |
| `agent_id`   | string (UUID) | The agent this session belongs to                                                                 |
| `room_url`   | string        | On Daily, the full room URL. On LiveKit, the server URL, which does not contain the room          |
| `room_name`  | string        | The room to join. Pass this to the cancel endpoint. Required by LiveKit, and correct on Daily too |
| `token`      | string        | Authentication token for joining the room                                                         |
| `provider`   | string        | `daily` or `livekit`. Which transport these credentials are for                                   |
| `timings`    | object        | Diagnostic timings. Unstable, do not depend on the shape                                          |

Use `room_url` and `token` to connect to the WebRTC room from your client.

{% hint style="warning" %}
**Branch on `provider`, do not assume Daily.** The transport is an organisation-level setting, so the same agent can return `daily` credentials today and `livekit` credentials after a settings change. Read `room_name` from the response rather than parsing it out of `room_url`: on LiveKit there is nothing to parse.
{% endhint %}

### Error Responses

**Branch on the HTTP status, not on a code string.** Most errors return the standard body below, where `message` is human-readable text and `error` is the HTTP reason phrase:

```json
{
  "statusCode": 404,
  "timestamp": "2026-09-08T09:12:34.567Z",
  "path": "/v1/public/agents/connect",
  "message": "Agent not found",
  "error": "Not Found"
}
```

A few cases carry a machine-readable slug under `error` instead of the reason phrase, plus extra fields:

```json
{
  "statusCode": 429,
  "error": "concurrency_limit",
  "message": "Agent has reached max concurrency (3)",
  "retry_after_seconds": 10
}
```

| HTTP Status | Meaning                                                                                                                                | Body notes                                                                |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `400`       | Request validation failed: `agent_id` is not a UUID, `client_user_ref` is over 255 characters, or the body carries an unknown property | `message` is an array of strings                                          |
| `402`       | The organisation is out of credits. Not retryable                                                                                      | `error` is `insufficient_credits`                                         |
| `403`       | Hostname allowlist check failed, or the agent is unpublished                                                                           | Standard body                                                             |
| `404`       | Agent id does not exist or is not visible to the caller                                                                                | Standard body                                                             |
| `409`       | A session for this caller is already being started. Recover with [Cancel by attempt](#cancel-a-session-by-attempt), then retry         | Carries `reason` (`session_already_in_flight`), `attempt_id` and `detail` |
| `429`       | The agent is at its concurrency limit                                                                                                  | `error` is `concurrency_limit`, plus `retry_after_seconds`                |
| `500`       | Unexpected server error                                                                                                                | Standard body                                                             |
| `502`       | The room could not be created, or an upstream service returned an error                                                                | Standard body                                                             |
| `503`       | Starting the session timed out. Retryable                                                                                              | Carries `reason` and `detail`                                             |

A `429` can also be relayed from upstream rather than raised by the API, in which case it carries `reason` and `detail` instead of `error`. Always read `retry_after_seconds` when it is present rather than assuming a fixed delay.

On a `409` you have no `room_name` yet, because no room was allocated. Recover through the attempt-keyed endpoint instead: `POST /v1/public/agents/cancel` with the same `X-Session-Attempt-Id` you sent on the failed connect, wait for it to return, then retry the connect. The room-keyed endpoint below cannot help here, since it needs a room name you never received.

### Authentication

Currently, the public Session API uses **hostname allowlist** authentication. When a browser request arrives, the server checks the `Origin` header against the domains configured on the agent:

* The origin's hostname must match a configured entry **exactly**, ignoring case. There is no subdomain or wildcard-suffix matching, so `app.example.com` is rejected by an allowlist containing only `example.com`. List every subdomain you serve from
* If no hostnames are configured, all requests are rejected (agent is private)
* If `*` is configured, all origins are accepted
* A missing or malformed `Origin` header is rejected with `403`

No API key or token is needed from the client side when using hostname allowlist.

{% hint style="info" %}
Server-side calls without a browser `Origin` header only work for agents configured with `*` in their allowed hostnames. Production server-to-server authentication is planned separately.
{% endhint %}

## Cancel a Session by Room

### `POST /v1/public/agents/connect/{room}`

Cancel an active public session after a room has been allocated. This is mainly used by browser clients during unload, tab-hide, or abort cleanup.

`{room}` is the `room_name` returned by `POST /v1/public/agents/connect`. Do not derive it from `room_url`: that only ever worked on Daily, and on LiveKit `room_url` carries no room name.

A room name over 128 characters returns `400`. Omitting the segment entirely does not reach this endpoint at all: the path no longer matches a route, so you get the API's route-not-found response rather than a `400`.

### Cancel Response (200 OK)

```json
{
  "released": 1
}
```

| Field      | Type   | Description                                                                         |
| ---------- | ------ | ----------------------------------------------------------------------------------- |
| `released` | number | Number of active sessions released. `0` means the room was already gone or unknown. |

## Cancel a Session by Attempt

### `POST /v1/public/agents/cancel`

Release a session that is stuck mid-start, before any room was allocated. This is the recovery path for a `409 session_already_in_flight`.

| Header                 | Required | Description                                                                                |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------ |
| `X-Session-Attempt-Id` | Yes      | The same attempt id you sent on the connect that returned `409`. Omitting it returns `400` |

There is no request body. The attempt id is a random per-client value, so this can only ever release your own stuck attempt, never another caller's session.

The response is the same shape as the room-keyed cancel:

```json
{
  "released": 1
}
```

Wait for it to return before retrying the connect.

## Examples

{% tabs %}
{% tab title="curl" %}

```bash
# Works only for wildcard/demo agents because curl does not attach a browser Origin.
curl -X POST https://api.ojin.ai/v1/public/agents/connect \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "550e8400-e29b-41d4-a716-446655440000"}'
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

# Works only for wildcard/demo agents because server-side HTTP clients do not
# attach a browser Origin.
response = requests.post(
    "https://api.ojin.ai/v1/public/agents/connect",
    json={"agent_id": "550e8400-e29b-41d4-a716-446655440000"}
)

data = response.json()
print(f"Session: {data['session_id']}")
print(f"Room: {data['room_url']}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const response = await fetch(
    "https://api.ojin.ai/v1/public/agents/connect",
    {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ agent_id: "550e8400-e29b-41d4-a716-446655440000" })
    }
);

const { session_id, room_url, room_name, token, provider } = await response.json();
```

Release the session when the user leaves the page:

```javascript
navigator.sendBeacon(
  `https://api.ojin.ai/v1/public/agents/connect/${encodeURIComponent(room_name)}`,
  ""
);
```

{% endtab %}
{% endtabs %}

## Connecting to the Room

The example below is for `provider: "daily"`. If the response says `livekit`, join with a LiveKit client instead, passing `room_url` as the server URL and `room_name` as the room.

After receiving the `room_url` and `token` from the Session API, connect to the WebRTC room using the [Daily JavaScript SDK](https://docs.daily.co/reference/daily-js):

```bash
npm install @daily-co/daily-js
```

```javascript
import DailyIframe from '@daily-co/daily-js';

// Create a call frame (attaches to DOM automatically)
const callFrame = DailyIframe.createFrame();

// Join the room with the token from the Session API
await callFrame.join({ url: room_url, token: token });

// The agent's audio and video tracks are now available
callFrame.on('track-started', (event) => {
    if (event.participant && !event.participant.local) {
        // Remote participant = the agent
        // event.track contains the audio or video MediaStreamTrack
    }
});
```

{% hint style="info" %}
This is only needed for custom integrations. If you're using the [widget](/apps/overview/widget-integration.md), it handles the room connection automatically.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ojin.ai/apps/overview/api-reference.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
