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.
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:
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.
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.
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
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);