Developer API — Tutorial
Build a voice agent via the API
Create, configure and activate an agent entirely over REST, then talk to it from a browser and watch the results arrive on a signed webhook. Everything below also works through the MCP server — same operations, driven from an AI assistant instead of curl.
1. Mint an API key
In the portal, open Developer → API keys and create a key with the read, write and admin scopes (admin is only needed for the webhook step). The secret is shown exactly once. API access requires the Scale plan or above.
2. Create a draft agent
definition is a partial document — anything you send is deep-merged over the platform defaults, so a minimal payload is a complete agent.
curl
curl -X POST https://api.loomira.ai/v1/agents \
-H "X-Api-Key: lmk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Reception Agent",
"definition": {
"greeting": { "texts": { "en": "Hello, you have reached Acme. How can I help?" } }
}
}'Response — 201 Created
{
"id": "6b1f9a1e-...",
"name": "Reception Agent",
"status": "draft",
"definition": { "schema_version": 1, "greeting": { "...": "..." }, "tools": [], "...": "..." },
"created_at": "2026-07-18T09:00:00Z",
"updated_at": "2026-07-18T09:00:00Z"
}3. Give it tools (optional)
Tools are created as standalone instances, then attached by id. A custom_webhook tool lets the agent call an HTTPS endpoint of yours mid-call:
curl — custom webhook tool
curl -X POST https://api.loomira.ai/v1/tools \
-H "X-Api-Key: lmk_..." \
-H "Content-Type: application/json" \
-d '{
"tool_type": "custom_webhook",
"name": "Create ticket",
"config": {
"url": "https://api.your-app.example/tickets",
"parameters": [
{ "name": "summary", "description": "One-line issue summary", "required": true }
]
}
}' The mcp_server tool type connects the voice agent to your own MCP server at call time — the agent discovers the server's tools and calls them mid-conversation. auth_header_secret is write-only (never echoed back; the response carries auth_header_secret_set: true instead), timeout_seconds accepts 5–30 (a caller is waiting on the line), and the optional tool_allowlist limits which remote tools the agent may see:
curl — MCP server tool
curl -X POST https://api.loomira.ai/v1/tools \
-H "X-Api-Key: lmk_..." \
-H "Content-Type: application/json" \
-d '{
"tool_type": "mcp_server",
"name": "Order system",
"config": {
"url": "https://mcp.your-app.example/mcp",
"label": "orders",
"auth_header_name": "Authorization",
"auth_header_secret": "Bearer your-server-token",
"timeout_seconds": 10,
"tool_allowlist": ["lookup_order", "reschedule_delivery"]
}
}'Response — 201 Created
{
"id": "0d3c7f42-...",
"tool_type": "mcp_server",
"name": "Order system",
"config": { "url": "https://mcp.your-app.example/mcp", "label": "orders",
"auth_header_secret_set": true, "...": "..." },
"enabled": true,
"created_at": "2026-07-18T09:01:00Z",
"updated_at": "2026-07-18T09:01:00Z"
}4. Attach the tools & finalize the definition
PUT /v1/agents/{id} replaces the full definition: fetch the current document with GET /v1/agents/{id}, edit it (add your tool ids to definition.tools — at most 4 mcp_server tools per agent), and send it back with the fetched updated_at as the If-Match header. If someone saved in between you get 412 Precondition Failed instead of a lost update.
curl
curl -X PUT https://api.loomira.ai/v1/agents/6b1f9a1e-... \
-H "X-Api-Key: lmk_..." \
-H "Content-Type: application/json" \
-H "If-Match: 2026-07-18T09:00:00Z" \
-d '{
"definition": {
"...": "the FULL definition document from GET /v1/agents/{id}",
"tools": ["0d3c7f42-..."]
}
}'5. Activate
Activation is the compliance and entitlement gate (Scale plan): the stored document is re-validated before going live.
curl
curl -X POST https://api.loomira.ai/v1/agents/6b1f9a1e-.../activate \
-H "X-Api-Key: lmk_..."Response — 200 OK
{
"id": "6b1f9a1e-...",
"name": "Reception Agent",
"status": "active",
"...": "..."
}6. Talk to it — two call paths
A. Server-side web-call mint
Mint a browser call session from your backend (the API key must never reach a browser) and hand the result to livekit-client:
curl
curl -X POST https://api.loomira.ai/v1/agents/6b1f9a1e-.../web-call \
-H "X-Api-Key: lmk_..."Response — 200 OK
{
"url": "wss://livekit.loomira.ai",
"token": "eyJhbGciOi...",
"room": "web-6b1f9a1e-...-a1b2c3d4"
}Browser (livekit-client)
import { Room } from "livekit-client";
const session = await fetch(
"https://api.loomira.ai/v1/agents/AGENT_ID/web-call",
{ method: "POST", headers: { "X-Api-Key": "lmk_..." } }, // server-side only!
).then(r => r.json());
const room = new Room();
await room.connect(session.url, session.token);
await room.localParticipant.setMicrophoneEnabled(true);B. Browser embed
Or skip the plumbing entirely: drop the <loomira-call> widget into your site — no API key in the page, origin-allowlisted, two lines of HTML. See the web embed guide.
7. Observe results with a webhook
Bind an endpoint to this agent (agent_id; omit it to receive every agent's events) and Loomira delivers signed events — the secret in the response is returned exactly once and signs every delivery.
curl
curl -X POST https://api.loomira.ai/v1/webhooks \
-H "X-Api-Key: lmk_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.your-app.example/webhooks/loomira",
"events": ["call.completed", "transcript.ready"],
"agent_id": "6b1f9a1e-..."
}'Response — 201 Created
{
"id": "9e5d1c88-...",
"url": "https://api.your-app.example/webhooks/loomira",
"events": ["call.completed", "transcript.ready"],
"enabled": true,
"agent_id": "6b1f9a1e-...",
"secret": "whsec_... <- shown exactly once, store it now",
"created_at": "2026-07-18T09:05:00Z",
"updated_at": "2026-07-18T09:05:00Z"
} Place a call, hang up, and call.completed followed by transcript.ready arrive at your endpoint. Signature verification and the full event list are covered in the quickstart & webhook guide.