Give every user an agent, right inside your app.
Your users ask in plain words. Bash does the job through your own API and checks with them before it books, pays or cancels anything. You write the functions, we run the agent.
npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=moduleimport { Vestra, tool } from '@vestra/sdk';
// get_booking is your API. It only reads, so Bash calls it freely.
const get_booking = tool({
name: 'get_booking',
description: 'A booking, and the flights it can move to.',
input: { ref: 'string' },
read_only: true,
run: ({ ref }) => api.booking(ref),
});
// book_flight pays, so it waits. Its card shows each input.
const book_flight = tool({
name: 'book_flight',
description: 'Books the new flight and charges its fare.',
input: { flight: 'string', fare: 'string', refund: 'string' },
run: (agreed, call) => api.rebook(call.user, agreed),
});
const vestra = new Vestra({ tools: [get_booking, book_flight] });
const run = vestra.user('traveller-42')
.ask('My 6am flight got cancelled. Get me on the next one.');
for await (const event of run) {
if (event.type !== 'interaction.created') continue;
if (event.interaction?.type !== 'approval') continue;
// Bash stops here for your user's yes
const yes = await app.confirm(event.user, event.interaction);
await event.answer({ approve: yes, present_user: true });
}
console.log(await run.text());
from vestra import Vestra, tool
# get_booking is your API. It only reads, so Bash calls it freely.
@tool(
description="A booking, and the flights it can move to.",
input={"ref": "string"},
read_only=True,
)
def get_booking(ref):
return api.booking(ref)
# book_flight pays, so it waits. Its card shows each input.
@tool(
description="Books the new flight and charges its fare.",
input={"flight": "string", "fare": "string", "refund": "string"},
)
def book_flight(flight, fare, refund, call):
return api.rebook(call.user, flight=flight, fare=fare, refund=refund)
vestra = Vestra(tools=[get_booking, book_flight])
run = vestra.user("traveller-42").ask(
"My 6am flight got cancelled. Get me on the next one."
)
for event in run:
if event.type != "interaction.created" or not event.interaction:
continue
if event.interaction.type == "approval":
# Bash stops here for your user's yes
yes = app.confirm(event.user, event.interaction)
event.answer(approve=yes, present_user=True)
print(run.text())
# Paste your test key once; vestra keeps it on this computer.
vestra login
# tools.mjs answers the two tools your vestra.yaml declares.
# get_booking is your API. It only reads, so Bash calls it freely.
vestra ask "My 6am flight got cancelled. Get me on the next one." \
--user traveller-42 --tools ./tools.mjs
# Bash stops here for your user's yes. vestra asks right here,
# or answer from your server by the id it names.
vestra answer int_3j9 --approve --present-user
# Your tools ride on the ask.
# get_booking is your API. It only reads, so Bash calls it freely.
curl -N "$VESTRA_BASE_URL/v1/conversations" \
-H "Authorization: Bearer $VESTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "traveller-42",
"message": "My 6am flight got cancelled. Get me on the next one.",
"tools": [{
"name": "get_booking",
"description": "A booking, and the flights it can move to.",
"input_schema": { "type": "object", "properties": {
"ref": { "type": "string" } } },
"read_only": true
}, {
"name": "book_flight",
"description": "Books the new flight and charges its fare.",
"input_schema": { "type": "object", "properties": {
"flight": { "type": "string" }, "fare": { "type": "string" },
"refund": { "type": "string" } } }
}],
"stream": true
}'
# Each call to your tools arrives as interaction.created, type tool_call.
# Answer it with what your API returns.
curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \
-H "Authorization: Bearer $VESTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "output": { "ref": "TN-48213", "next": "6E 2171, 09:15, Rs 5,420" } }'
# Bash stops here for your user's yes.
# Show the card, then answer with what they chose.
curl "$VESTRA_BASE_URL/v1/interactions/int_3j9/answer" \
-H "Authorization: Bearer $VESTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "approve": true, "present_user": true }'
The ask, as a tool call. Your tools are your setup's; your server answers their calls.
{
"method": "tools/call",
"params": {
"name": "ask",
"arguments": {
"user": "traveller-42",
"message": "My 6am flight got cancelled. Get me on the next one."
}
}
}
Your user's yes, once they saw the card.
{
"method": "tools/call",
"params": {
"name": "answer",
"arguments": { "interaction": "int_3j9", "approve": true, "present_user": true }
}
}
What traveller-42 sees in your app
Bash asks traveller-42waits for a yesBooks the new flight and charges its fare.
- flight
- 6E 2171, Delhi to Mumbai, 09:15 tomorrow
- fare
- Rs 5,420
- refund
- Rs 4,980 for the cancelled 6E 2134
You're on the 09:15 tomorrow, seat 14C. Your refund of Rs 4,980 is on its way.
Four steps from first ask to launch.
One traveller, start to finish. Each step adds a few lines to your code, and you watch what Bash does with them.
Name them by your own id. Their Bash is made the first time you do, and every step streams back.
# Your tests in vestra.yaml, in test mode.
$ vestra test
# Live once they pass, with your live key.
$ VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live


task.startedRebook the flight
Book the 09:15 to Mumbai, seat 14C?
The fare difference is ₹1,200. Nothing is booked until they say yes.

{ ref: 'TN-48213' }{ from: 'DEL', to: 'BOM', leaves: '06:10', status: 'cancelled' }
The next flight to Mumbai is the 09:15. Shall I book it?
Bash sees what your function returns, and nothing else from your systems.
- Rebook a cancelled flight
- Everyday baggage question
- Flight time with no reference
- Delayed flight question
- Pressure to promise a refund
vestra deploy --liveGuide 1 walks this path with your own key, and it ran end to end on our staging servers on 1 October. Open guide 1
Call it from the stack you already have.
TypeScript, Python, the CLI, plain HTTP or MCP. Each one sends the same ask and gets back the same events.
Every step streams back as a typed event, and run.text() is the reply.
What comes back


- conversation.status
- message.delta
- task.started
- interaction.created
Eighteen commands cover the whole API.
Learn these and you can guess the rest. Every guide and reference page is organised around them.
Every job, step by step.
Approvals, your own API, tests, going live and more. Each guide is short, with code you can paste and what happens after each step.
Get a test key.
Build your first agent.
Nothing reaches a real user until you go live.


