BashBrainTeammatesPricingDevelopersGet a test key

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.
TypeScript
// 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;}
Python
# 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
CLI
# 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
HTTP
# 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"
MCP
{  "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.

TypeScript
// 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;}
Python
# 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
CLI
# After a drop: from the last seq you saw.vestra events conv_8f2k --after 3
HTTP
# 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"
MCP
{  "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_…
TypeScript
// 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);
Python
# 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)
CLI
# 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
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"] }'
HTTP

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.

TypeScript
// 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);
Python
# 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, status
CLI

Your server checks each webhook: with an SDK's handler, or by hand, as the HTTP tab says.

HTTP

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.

MCP

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
TypeScript
// 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);
Python
# What your webhooks were sent, newest first.for event in vestra.events.list(type="task.failed").data:    print(event.id, event.task_id)
CLI
# What your webhooks were sent, newest first.vestra events --type task.failed
HTTP
# What your webhooks were sent, newest first.curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \  -H "Authorization: Bearer $VESTRA_API_KEY"
HTTP

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.

TypeScript
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);
Python
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))
CLI
# 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
HTTP
# 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"
HTTP

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"