Events and webhooks
Every step Bash takes is an event. Follow one conversation as it happens, or have the lasting ones sent to your server, signed.
Follow a conversation
Its kept events come first, then each one as it happens (events).
What happens Each event carries a seq, so you always know where you are.
Show outputHide output
The CLI prints
1 conversation.status replying (running) 3 task.started Rebook the flight (task_91d) 4 interaction.created approval int_3j9 5 message.completed On it. I will ask before I pay. 7 task.completed Rebooked on the 09:15.
// Its kept events first, then each one as it happens.for await (const event of vestra.events('conv_8f2k')) { console.log(event.seq, event.type); if (event.type === 'task.completed') break;}# Its kept events first, then each one as it happens.for event in vestra.events("conv_8f2k"): print(event.seq, event.type) if event.type == "task.completed": break# Its kept events first, then each one as it happens, until it is idle with no open work.# --follow keeps watching after that, until Ctrl-C.vestra events conv_8f2k# Server-Sent Events: its kept events first, then each one as it happens.curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events" \ -H "Authorization: Bearer $VESTRA_API_KEY"{ "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } }}Pick up after a drop
Reconnect with the last seq you saw.
What happens It starts at the next event: nothing is missed, and nothing comes twice.
// After a drop: from the last seq you saw.for await (const event of vestra.events('conv_8f2k', { after: 3 })) { console.log(event.seq, event.type); if (event.type === 'task.completed') break;}# After a drop: from the last seq you saw.for event in vestra.events("conv_8f2k", after=3): print(event.seq, event.type) if event.type == "task.completed": break# After a drop: from the last seq you saw.vestra events conv_8f2k --after 3# After a drop: reconnect with the last seq you saw.curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Last-Event-ID: 3"{ "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 3 } }}Send them to your server
Webhooks get the events worth keeping: a task done, an approval asked. The reply as it is typed comes only on the stream.
What happens Bash signs each one with your endpoint's secret, shown this once.
Show outputHide output
The CLI prints
Sending task.completed, interaction.created to https://api.tripnest.example/vestra as whk_2bq. Its signing secret, shown this once: whsec_…
// Once. Keep the secret as VESTRA_WEBHOOK_SECRET: it is shown once.const url = 'https://api.tripnest.example/vestra';const endpoint = await vestra.events.to(url, { events: ['task.completed', 'interaction.created'],});console.log(endpoint.secret);# Once. Keep the secret as VESTRA_WEBHOOK_SECRET: it is shown once.endpoint = vestra.events.to( "https://api.tripnest.example/vestra", events=["task.completed", "interaction.created"],)print(endpoint.secret)# Once. Keep the secret as VESTRA_WEBHOOK_SECRET: it is shown once.vestra events --to https://api.tripnest.example/vestra --type task.completed,interaction.created# Once. Keep the secret as VESTRA_WEBHOOK_SECRET: it is shown once.curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tripnest.example/vestra", "events": ["task.completed", "interaction.created"] }'Webhooks are set up once, from an SDK, the CLI or HTTP.
# Once. Keep the secret as VESTRA_WEBHOOK_SECRET: it is shown once.curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tripnest.example/vestra", "events": ["task.completed", "interaction.created"] }'Check each one
The handler checks the signature with VESTRA_WEBHOOK_SECRET, then hands you the event. Give it the raw body: a parsed one fails the check.
What happens A signature it cannot trust, or one over 300 seconds old, is answered 400, and a good one 200. Anything but a 2xx is sent again later.
// Checks each signature with VESTRA_WEBHOOK_SECRET,// then hands you the event.const handler = vestra.events.handler(async (event) => { if (event.type === 'task.completed') console.log(event.task_id);}); // The raw body, as it came: a parsed one fails the check.const raw = express.raw({ type: 'application/json' });app.post('/vestra', raw, handler);# Checks each signature with VESTRA_WEBHOOK_SECRET,# then hands you the event.def on_event(event): if event.type == "task.completed": print(event.task_id) handler = vestra.events.handler(on_event) @app.post("/vestra")def vestra_webhook(): # the raw body, as it came: a parsed one fails status, body = handler(request.get_data(), request.headers) return body, statusYour server checks each webhook: with an SDK's handler, or by hand, as the HTTP tab says.
Vestra-Signature: t=<unix seconds>,v1=<hex> is an HMAC-SHA256 of <t>.<raw body> with your secret. Refuse it when it does not match, or when t is over 300 seconds old.
Webhooks reach your server, not an agent: check them with an SDK's handler.
Read what you missed
The record of what your webhooks were sent, newest first.
What happens after an event's id reads forward from it, so a server back from a restart catches up.
Show outputHide output
The CLI prints
id type user conversation_id task_id created_at evt_4hq task.failed traveller-42 conv_8f2k task_91d 2026-10-02T09:14:03.000Z
// What your webhooks were sent, newest first.const failed = await vestra.events.list({ type: 'task.failed' });for (const event of failed.data) console.log(event.id, event.task_id);# What your webhooks were sent, newest first.for event in vestra.events.list(type="task.failed").data: print(event.id, event.task_id)# What your webhooks were sent, newest first.vestra events --type task.failed# What your webhooks were sent, newest first.curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY"The record is read from an SDK, the CLI or HTTP. An agent follows a conversation with the events tool.
# What your webhooks were sent, newest first.curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY"The whole thing
Follow, send, check and catch up, in one program.
What happens Live while it runs, signed at your server, and a record for anything missed.
import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // Follow a conversation, or pick up after a dropfor await (const event of vestra.events('conv_8f2k', { after: 3 })) { console.log(event.seq, event.type); if (event.type === 'task.completed') break;} // Send events to your serverawait vestra.events.to('https://api.tripnest.example/vestra', { events: ['task.completed', 'interaction.created'],}); // Check each oneconst handler = vestra.events.handler(async (event) => { if (event.type === 'task.completed') console.log(event.task_id);});const raw = express.raw({ type: 'application/json' });app.post('/vestra', raw, handler); // Read what you missedconst failed = await vestra.events.list({ type: 'task.failed' });console.log(failed.data.length);from vestra import Vestra vestra = Vestra() # Follow a conversation, or pick up after a dropfor event in vestra.events("conv_8f2k", after=3): print(event.seq, event.type) if event.type == "task.completed": break # Send events to your servervestra.events.to( "https://api.tripnest.example/vestra", events=["task.completed", "interaction.created"],) # Check each onedef on_event(event): if event.type == "task.completed": print(event.task_id) handler = vestra.events.handler(on_event) @app.post("/vestra")def vestra_webhook(): status, body = handler(request.get_data(), request.headers) return body, status # Read what you missedprint(len(vestra.events.list(type="task.failed").data))# Follow a conversation, or pick up after a dropvestra events conv_8f2k --after 3 # Send events to your servervestra events --to https://api.tripnest.example/vestra --type task.completed,interaction.created # Read what you missedvestra events --type task.failed# Follow a conversation, or pick up after a dropcurl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Last-Event-ID: 3" # Send events to your servercurl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tripnest.example/vestra", "events": ["task.completed", "interaction.created"] }' # Read what you missedcurl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY"An agent follows a conversation with the events tool. Webhooks are your server's.
# Follow a conversation, or pick up after a dropcurl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Last-Event-ID: 3" # Send events to your servercurl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tripnest.example/vestra", "events": ["task.completed", "interaction.created"] }' # Read what you missedcurl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY"