oxDocs
Browse documentation
Introduction
Vision API
Inference API
Realtime
Integrations
LiveKit & SIP
Administration

Project management

Project control plane
Reference

Project management

Project control plane

Manage projects, scoped credentials, agents, webhooks, usage, audit logs, and session history.

Create project credentials

Project administration requires a separate control-plane client and credential. Never initialize it with a customer API key or expose its token to browser code.

TypeScript

import { OxControlPlane } from "@ox/sdk";

const token = process.env.OX_CONTROL_PLANE_ADMIN_TOKEN;
if (!token) throw new Error("OX_CONTROL_PLANE_ADMIN_TOKEN is required");

const controlPlane = new OxControlPlane({ token });
const project = await controlPlane.projects.create({
  name: "Acme Voice",
  contactEmail: "developer@example.com",
});
const issued = await controlPlane.apiKeys.create(project.id, {
  name: "Project automation",
  scopes: ["project:read", "project:write", "project:keys"],
});

await deliverKeyOnceToAuthorizedCustomer(issued.value);

issued.value is returned once. Do not log it, store it in source control, or send it to an unauthorized client. projects supports list, create, retrieve, suspend, and reactivate. apiKeys supports list, create, and revoke.

Use a project-scoped management key

Customers never receive the global control-plane token. Bind the scoped key to its project in trusted server code:

TypeScript

import { OxProject } from "@ox/sdk";

const project = new OxProject({
  projectId: process.env.OX_PROJECT_ID!,
  apiKey: process.env.OX_PROJECT_MANAGEMENT_KEY!,
});

const agents = await project.agents.list();
const sessions = await project.sessions.list({ limit: 25 });

Management keys require explicit project:read, project:write, or project:keys scopes, cannot cross project boundaries, and cannot issue keys with scopes they do not possess. Realtime-only keys retain only realtime and cannot access management routes.

Register webhooks and inspect usage

Register realtime-session and Hallikar incident webhooks and store the returned signing secret once. Verify delivery signatures against the unmodified request body before parsing JSON.

TypeScript

const endpoint = await controlPlane.webhooks.create(projectId, {
  url: "https://example.com/webhooks/ox",
  events: ["realtime.session.completed", "realtime.session.failed", "hallikar.incident.opened"],
});
await storeWebhookSecret(endpoint.secret);

const usage = await controlPlane.usage.list(projectId, { days: 30 });
const audit = await controlPlane.auditLogs.list(projectId, { limit: 50 });
const invitation = await controlPlane.dashboardInvitations.create(projectId);

Webhook endpoints support list, create, and disable. Dashboard invitation tokens are short-lived and should be delivered only to the intended project operator.

Manage agents and inspect session history

Agents are reusable, project-scoped configurations. Creating an agent produces version 1; supplying a new configuration through agents.update appends an immutable version. Name-only updates do not create a configuration version.

TypeScript

const agent = await controlPlane.agents.create(projectId, {
  name: "Support",
  configuration: {
    instructions: "Resolve account questions and confirm before changing data.",
    voice: "Ryan",
    endpointing: { eagerness: "medium" },
  },
});

const updated = await controlPlane.agents.update(projectId, agent.id, {
  configuration: {
    ...agent.versions.at(-1)?.configuration,
    instructions: "Resolve account questions. Escalate refund requests.",
  },
});

const sessions = await controlPlane.sessions.list(projectId, {
  limit: 25,
  status: "completed",
  transport: "websocket",
});
const latest = sessions.data[0]
  ? await controlPlane.sessions.retrieve(projectId, sessions.data[0].id)
  : undefined;

Use agents.list, agents.retrieve, and agents.archive for lifecycle management. Archived agents remain available with agents.list(projectId, { includeArchived: true }). Session records contain project metadata, configuration, routing, trace identifiers, and metered audio usage when those fields have been recorded; they never contain the one-time session secret.

Verify webhook signatures

TypeScript

import { verifyWebhook } from "@ox/sdk";

const rawBody = await request.text();
const secret = process.env.OX_WEBHOOK_SECRET;
if (!secret) throw new Error("OX_WEBHOOK_SECRET is required");

const valid = await verifyWebhook({
  secret,
  body: rawBody,
  headers: {
    id: request.headers.get("webhook-id") ?? "",
    timestamp: request.headers.get("webhook-timestamp") ?? "",
    signature: request.headers.get("webhook-signature") ?? "",
  },
});

if (!valid) return new Response("Invalid signature", { status: 401 });
const event = JSON.parse(rawBody);
Help improve this guideFound something unclear or incomplete?
Report an issue ↗View source ↗