Skip to content

Sessions

A session is a single realtime avatar run — the headline Protoface resource. This guide covers the full lifecycle from your code. For the interactive reference, see the API Reference.

POST /v1/sessions validates your request, allocates a sess_... ID, queues a worker job, and returns 201 with status: "created". It returns in under 300 ms — placement onto a worker happens asynchronously.

import { ProtofaceClient } from "@protoface/sdk";
const client = new ProtofaceClient({ apiKey: process.env.PROTOFACE_API_KEY! });
const session = await client.sessions.createLivekit({
avatarId: "av_demo",
url: process.env.LIVEKIT_URL!,
roomName: "demo-room",
workerToken, // minted with your LiveKit key/secret
quality: "mock",
maxDurationSeconds: 600,
idleTimeoutSeconds: 30,
metadata: { user_id: "u_123" },
});

| Field | Required | Notes | | ---------------------- | -------- | -------------------------------------------------------------- | | avatar_id | yes | An av_... ID. List with GET /v1/avatars. | | transport | yes | A TransportConfig. See Transports. | | quality | no | mock | lite | standard | pro. v0 renders mock. | | max_duration_seconds | no | Hard cap; worker terminates on hit. Default 600, max 3600. | | idle_timeout_seconds | no | End after this long with no audio. Default 30. | | metadata | no | Free-form JSON, ≤ 8 KB, echoed on reads and webhooks. |

See Limits & quotas for the exact bounds and reserved metadata keys.

| Status | Meaning | | ---------- | ------------------------------------------------------------- | | created | Accepted and queued. | | queued | Worker job written; awaiting claim. | | starting | Worker claimed the job and is initializing. | | running | Avatar is publishing video. | | ending | Draining toward a clean stop. | | ended | Clean termination. | | failed | Terminal failure — see failure.code / failure.message. | | canceled | Ended before it ran (via end). |

The session object also carries timing fields you can use to measure the realtime targets: created_at, started_at, first_frame_at, ended_at, failed_at, plus a live usage roll-up ({ billable_seconds, frames }).

GET /v1/sessions/{session_id} returns the full session. The usage block reflects the latest worker heartbeat and may lag by one heartbeat interval.

const s = await client.sessions.get("sess_01HXY...");
console.log(s.status, s.first_frame_at, s.usage.billable_seconds);

GET /v1/sessions returns sessions newest-first with cursor pagination.

const page = await client.sessions.list({
status: ["running", "starting"],
limit: 20,
});
for (const s of page.data) console.log(s.id, s.status);
if (page.hasMore) {
const next = await client.sessions.list({ startingAfter: page.nextCursor! });
}
  • status — repeatable; multiple values are OR’d. Also accepts the group aliases active (= created | queued | starting | running | ending) and terminal (= ended | failed | canceled).
  • avatar_id — single value.
  • created_after, created_before — ISO-8601 timestamps.
  • limit, starting_after — cursor pagination.

List responses use the standard envelope:

{
"object": "list",
"data": [ /* sessions */ ],
"has_more": true,
"next_cursor": "sess_01HXY..."
}

POST /v1/sessions/{session_id}/end is idempotent. Its effect depends on the current status:

| Current status | Effect | | ------------------------ | ---------------------------------------------------------- | | created, queued | Transition to canceled; pending job is canceled. | | starting | Cancel signal sent; becomes canceled after ack. | | running | Transition to ending; worker drains then ended. | | ending | No-op; returns current session. | | ended/failed/canceled | No-op; returns current session with 200. |

It returns the session in all cases, or 404 not_found if the ID doesn’t exist or isn’t accessible to your key.

await client.sessions.end("sess_01HXY...");

POST /v1/sessions accepts an optional Idempotency-Key header. Send a unique key per logical create and you get safe retries: the same key + same body within 24 hours returns the original response; the same key + a different body returns 409 conflict. Omit the header and the request simply executes once.

await client.sessions.create(request, { idempotencyKey: crypto.randomUUID() });

Every non-2xx response uses the uniform error envelope. Switch on the stable error.code, never the message. The SDK throws typed subclasses (RateLimitError, QuotaExceededError, …) and retries 429/503 for you. The POST /v1/sessions rate limit is 10 requests/min/key — see Limits & quotas.