Quick start: receive events
Vocal records an event for each change that matters, such as a published Agent version or an ended Call. In this quick start you read the event catalog, make an event filter, read the event log and the Organization stream through it, and see what a replay answers when it names no Webhook subscription. It uses curl and an API key; no SDK is involved.
Delivery of events to an HTTPS endpoint of yours takes a Webhook subscription. Contracts 1.63.0 adds the operations that create and manage one (Webhooks), and each of their reference pages says whether the Phase 0 host serves it. This quick start creates no subscription: you receive events by reading them, from the log or live from the stream.
On the Phase 0 host. Served. The six operations below were first released in 1.52.0, and the api on the Phase 0 host serves every one of them. The replay is served too, but this page creates no subscription, so it finds none. 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 webhooks:read, webhooks:write and events:read, and the id of an Agent: use the one from Your first Agent. Events carry identifiers, status, codes and instants only, never content.
export VOCAL_API=https://api.example.test
export VOCAL_API_KEY=vk_test_...
1. Read the event catalog
GET /v1/event-types lists every event type with its family, its class and the JSON Schema of its data. family and class filter it.
curl "$VOCAL_API/v1/event-types?limit=100" \
-H "Authorization: Bearer $VOCAL_API_KEY"const response = await fetch(`${process.env.VOCAL_API}/v1/event-types?limit=100`, {
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/event-types",
params={
"limit": "100",
},
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
},
)
print(response.status_code, response.text)Look for data, an array of event types. Each one has a type, a family, a class (record or trace), the since release that listed it, a description and the data_schema of its payload. The answer also carries object, which is "list", and has_more. The event types page lists the same types.
2. Create an event filter
An event filter narrows the log and the stream to the events of the Agents, Phone numbers or tags it names. POST /v1/event-filters creates one. A filter that selects nothing is refused with 422 validation_failed.
curl -X POST "$VOCAL_API/v1/event-filters" \
-H "Authorization: Bearer $VOCAL_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "quick-start-agent", "agent_ids": ["agt_..."] }'const response = await fetch(`${process.env.VOCAL_API}/v1/event-filters`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "quick-start-agent",
"agent_ids": [
"agt_..."
]
}),
});
console.log(response.status, await response.text());import os
import uuid
import requests
response = requests.post(
f"{os.environ['VOCAL_API']}/v1/event-filters",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "quick-start-agent",
"agent_ids": [
"agt_...",
],
},
)
print(response.status_code, response.text)The answer is 201. Look for object, which is "event_filter", id beginning evf_, revision, which is 1, and agent_ids, which holds your Agent. Keep the id.
3. Read the event log through the filter
GET /v1/events pages the Organization's record events of the last 30 days (a target), newest first by sequence, for the environment of your key.
curl "$VOCAL_API/v1/events?filter_id=evf_...&limit=10" \
-H "Authorization: Bearer $VOCAL_API_KEY"const response = await fetch(`${process.env.VOCAL_API}/v1/events?filter_id=evf_...&limit=10`, {
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/events",
params={
"filter_id": "evf_...",
"limit": "10",
},
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
},
)
print(response.status_code, response.text)Look for data, an array of events, has_more and object. An empty data is an answer, not a failure: it means no event matches the filter yet. Keep the id of the first event if there is one.
4. Read one event
GET /v1/events/{event_id} answers one event of the log as a delivery carried it.
curl "$VOCAL_API/v1/events/evt_..." \
-H "Authorization: Bearer $VOCAL_API_KEY"const response = await fetch(`${process.env.VOCAL_API}/v1/events/evt_...`, {
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/events/evt_...",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
},
)
print(response.status_code, response.text)Look for object, which is "event", id, type, family, sequence, api_version, which is "v1", livemode, which is false for a test key, created_at and data. De-duplicate on id, and use sequence to discard a stale update. In the run of 2026-10-24 UTC the log held no event for the filter yet, so the runner skipped this step; in two earlier runs that day it held events.
5. Open the Organization stream
GET /v1/events/stream answers text/event-stream: every record event of your environment as it is relayed, at least once. filter_id selects as on the log, and Last-Event-ID resumes after the last event you received.
curl -N "$VOCAL_API/v1/events/stream?filter_id=evf_..." \
-H "Authorization: Bearer $VOCAL_API_KEY" \
-H "Accept: text/event-stream"const response = await fetch(`${process.env.VOCAL_API}/v1/events/stream?filter_id=evf_...`, {
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
Accept: 'text/event-stream',
},
});
console.log(response.status);
for await (const chunk of response.body) {
process.stdout.write(chunk);
}import os
import requests
response = requests.get(
f"{os.environ['VOCAL_API']}/v1/events/stream",
params={
"filter_id": "evf_...",
},
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
"Accept": "text/event-stream",
},
stream=True,
)
print(response.status_code)
for line in response.iter_lines(decode_unicode=True):
print(line)Look for the status 200 and the media type text/event-stream. Each message has an id:, an event: and a data: line. The connection stays open, so stop curl when you have seen enough; the runner closes it as soon as it has read the status and the media type. A credential has at most three connections at a time (a target).
6. Replay a window to a Webhook subscription
POST /v1/webhooks/{webhook_id}/replay re-sends the events of a time window to a Webhook subscription. This quick start creates no subscription, so the webhook_id below, the example identifier of the public document, names none. The window must lie inside the last 30 days and end before the request.
curl -X POST "$VOCAL_API/v1/webhooks/whk_.../replay" \
-H "Authorization: Bearer $VOCAL_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "from": "2026-10-22T00:00:00.000Z", "to": "2026-10-22T12:00:00.000Z" }'const response = await fetch(`${process.env.VOCAL_API}/v1/webhooks/whk_.../replay`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VOCAL_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"from": "2026-10-22T00:00:00.000Z",
"to": "2026-10-22T12:00:00.000Z"
}),
});
console.log(response.status, await response.text());import os
import uuid
import requests
response = requests.post(
f"{os.environ['VOCAL_API']}/v1/webhooks/whk_.../replay",
headers={
"Authorization": f"Bearer {os.environ['VOCAL_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"from": "2026-10-22T00:00:00.000Z",
"to": "2026-10-22T12:00:00.000Z",
},
)
print(response.status_code, response.text)The answer is 404 not_found, and the runner checks exactly that. The identifier above is a placeholder that no subscription has. In the run of 2026-10-24 UTC the replay answered 404 not_found, as described.
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 |
|---|---|---|
GET /v1/event-types | 1.52.0 | Served |
POST /v1/event-filters | 1.52.0 | Served |
GET /v1/events | 1.52.0 | Served |
GET /v1/events/{event_id} | 1.52.0 | Served |
GET /v1/events/stream | 1.52.0 | Served |
POST /v1/webhooks/{webhook_id}/replay | 1.52.0 | Served, and finds no subscription |
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.