Quick start: place a web Call
A web Call connects a caller in a browser to an Agent. In this quick start your server creates the Call, reads it, ends it and reads its trace, with curl and an API key; no SDK is involved. It needs an Agent with a published, active version: use the one from Your first Agent.
On the Phase 0 host. Served. The four operations below were first released in 1.19.0 or earlier, and the api on the Phase 0 host serves every one of them. On 2026-10-24 UTC the runner passed the 28 steps of the four quick starts against the Phase 0 host, which has pinned contracts 1.60.0 since deploy #70, with a
livekey: 27 steps were answered and one was skipped, as its page allows. No run with atestkey is recorded.
Before you start
You need an API key whose scopes include calls:read and calls:write, and the id of an Agent whose active version Calls can run: the one from Your first Agent will do. A test key has a daily allowance of Call minutes, and a Call on mock Providers never counts against it; Your first Agent explains which Providers an Agent may name. Replace the placeholders with your own values.
export VOCAL_API=https://api.example.test
export VOCAL_API_KEY=vk_test_...
A browser that joins the Call speaks to the Agent through a LiveKit client; this quick start stays on the HTTP side and does not join. Place a web Call covers the browser.
1. Create the Call
POST /v1/calls runs admission and answers with the Call and what a browser needs to join it. The Idempotency-Key is required.
curl -X POST "$VOCAL_API/v1/calls" \
-H "Authorization: Bearer $VOCAL_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "transport": "web", "agent_id": "agt_..." }'const response = await fetch(`${process.env.VOCAL_API}/v1/calls`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"transport": "web",
"agent_id": "agt_..."
}),
});
console.log(response.status, await response.text());import os
import uuid
import requests
response = requests.post(
f"{os.environ['VOCAL_API']}/v1/calls",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"transport": "web",
"agent_id": "agt_...",
},
)
print(response.status_code, response.text)The answer is 201. Look for object, which is "call", id beginning call_, status, which is queued in Phase 0, transport, which is "web", agent_id, and client, an object with url, token and expires_at. The token is a secret for this Call only: hand it to the browser and never log it. Admission can refuse the Call, among others with 402 spend_cap_reached, 409 agent_version_unavailable and 429 concurrency_limit_reached; the operation page lists every answer.
2. Read the Call
GET /v1/calls/{call_id} answers the Call as it stands.
curl "$VOCAL_API/v1/calls/call_..." \
-H "Authorization: Bearer $VOCAL_API_KEY"const response = await fetch(`${process.env.VOCAL_API}/v1/calls/call_...`, {
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
},
});
console.log(response.status, await response.text());import os
import requests
response = requests.get(
f"{os.environ['VOCAL_API']}/v1/calls/call_...",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
},
)
print(response.status_code, response.text)Look for object, id, status and transport.
3. End the Call
An HTTP-only run has no conversation to wait for, so end the Call. POST /v1/calls/{call_id}/end ends it with the ended reason canceled; the reason_code is a snake_case code recorded in the Audit event and is never free text.
curl -X POST "$VOCAL_API/v1/calls/call_.../end" \
-H "Authorization: Bearer $VOCAL_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "reason_code": "quick_start_done" }'const response = await fetch(`${process.env.VOCAL_API}/v1/calls/call_.../end`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"reason_code": "quick_start_done"
}),
});
console.log(response.status, await response.text());import os
import uuid
import requests
response = requests.post(
f"{os.environ['VOCAL_API']}/v1/calls/call_.../end",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"reason_code": "quick_start_done",
},
)
print(response.status_code, response.text)The answer is 200 with the Call as it stands once the request is recorded: look for object and id. A Call that has already ended answers 409 call_not_active, which is not a failure here. A Call that no worker had claimed ends at once; otherwise the worker that runs it stops the Agent audio, ends the Session and finalizes.
4. Read the Call until it has ended
Read the Call again until status is ended.
curl "$VOCAL_API/v1/calls/call_..." \
-H "Authorization: Bearer $VOCAL_API_KEY"const response = await fetch(`${process.env.VOCAL_API}/v1/calls/call_...`, {
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
},
});
console.log(response.status, await response.text());import os
import requests
response = requests.get(
f"{os.environ['VOCAL_API']}/v1/calls/call_...",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
},
)
print(response.status_code, response.text)Look for status, which becomes ended, and ended_reason, which is canceled when your end request was the cause. The answer also carries object and id. Wait a second between reads.
5. Read the trace
The trace explains a finalized Call. GET /v1/calls/{call_id}/trace answers it as segments.
curl "$VOCAL_API/v1/calls/call_.../trace" \
-H "Authorization: Bearer $VOCAL_API_KEY"const response = await fetch(`${process.env.VOCAL_API}/v1/calls/call_.../trace`, {
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
},
});
console.log(response.status, await response.text());import os
import requests
response = requests.get(
f"{os.environ['VOCAL_API']}/v1/calls/call_.../trace",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
},
)
print(response.status_code, response.text)There are two documented outcomes. A Call that ran, which is one a worker had claimed, was finalized, and the answer is 200: look for object, which is "call_trace", call_id, agent_id and segments. A Call that ended at once because no worker had claimed it never ran and was not finalized, so it has no trace and the read answers 404 not_found. To see a trace with segments, join the Call from a browser, talk to the Agent, and read the trace after the Call has ended; the Call trace guide explains the segments. In the run of 2026-10-24 UTC this read answered 404 not_found, the outcome for a Call that never ran, since no browser joined the Call.
Operations and releases
Every operation here is served by the Phase 0 host. The release is the one that first served the operation.
| Operation | First served in | Phase 0 host |
|---|---|---|
POST /v1/calls | 1.0.0 | Served |
GET /v1/calls/{call_id} | 1.0.0 | Served |
POST /v1/calls/{call_id}/end | 1.19.0 | Served |
GET /v1/calls/{call_id}/trace | 1.3.0 | Served |
Run it
The repository that builds these pages holds a runner that sends these requests in order, with the Agent of the first quick start, checks each answer's status and the fields named above, and prints one line per step. See HTTP quick starts for the command and for the record of its run.