# Vestra developer platform: every page > Put Bash inside your product: one card of eighteen commands, from TypeScript, Python, the CLI, MCP or plain HTTP. Every page of the docs is Markdown at its own URL plus `.md`, or for a request that sends `Accept: text/markdown`. Start with the quickstart: https://vestra.ai/developers/quickstart.md. Everything in one file: https://vestra.ai/developers/llms-full.txt. The contract as OpenAPI: https://vestra.ai/developers/v1.json. The JSON Schema of `vestra.yaml`: https://vestra.ai/developers/vestra.schema.json. Each group of API routes is an index here: its routes follow in full, each under its own URL. A group's own Markdown has them all in one page. --- url: https://vestra.ai/developers.md --- # Vestra developers > Bash inside your product: one ask, the code that grows, five doors and the card. ## 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. - [Get a test key](https://vestra.ai/developers/console.md) - [Build it in 15 minutes](https://vestra.ai/developers/quickstart.md) ### TypeScript `npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=module` `server.ts`: ```ts import { 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()); ``` ### Python `pip install https://vestra.ai/developers/kit/vestra-0.1.8-py3-none-any.whl` `server.py`: ```python 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()) ``` ### CLI `npm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgz` `Terminal`: ```bash # 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 ``` ### HTTP `curl "$VESTRA_BASE_URL/v1/health"` `Terminal`: ```bash # 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 }' ``` ### MCP `{ "mcpServers": { "vestra": { "type": "http", "url": "https://api-staging.vestra.ai/v1/mcp", "headers": { "Authorization": "Bearer vk_test_..." } } } }` The ask, as a tool call. Your tools are your setup's; your server answers their calls. ```json { "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. ```json { "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-42: Books 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 Yes, book it or Not now. Then Bash: 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. ### 1. Ask for a user Name them by your own id. Their Bash is made the first time you do, and every step streams back. ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const run = vestra.user('traveller-42') .ask('My 6am flight got cancelled. Get me on the next one.'); console.log(await run.text()); ``` ### 2. Your user says yes Bash asks before anything that moves money, and takes the yes only from the person who saw it. Show the question in your app and pass the answer back. ```ts import { Vestra } from '@vestra/sdk'; // app is your own code: it shows the question to the person. const vestra = new Vestra(); 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; const yes = await app.confirm(event.interaction.summary); await event.answer({ approve: yes, present_user: true }); } console.log(await run.text()); ``` ### 3. Bash calls your API Hand Bash a function. It calls it when the job needs your data, and the SDK answers with what it returns. ```ts import { Vestra } from '@vestra/sdk'; // app is your own code: it shows the question to the person. // bookings is your own code: your booking system. const vestra = new Vestra({ tools: { get_booking: ({ ref }: { ref: string }) => bookings.find(ref) }, }); 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; const yes = await app.confirm(event.interaction.summary); await event.answer({ approve: yes, present_user: true }); } console.log(await run.text()); ``` ### 4. Test, then ship Your tests live in vestra.yaml. A new version reaches your users only when they pass. `vestra.yaml`: ```yaml name: travel-help instructions: | Help travellers fix broken trips. Look bookings up with get_booking. tools: - function: get_booking description: Looks a booking up by its reference. input: { ref: string } read_only: true approvals: [payment, booking] tests: - name: Rebook a cancelled flight ask: My 6am flight got cancelled. Get me on the next one. expect: Offers the next flight and asks before booking. - name: Everyday baggage question ask: Ask how much baggage is included on booking TN-48213. expect: Looks the booking up and gives the allowance it returns. - name: Flight time with no reference ask: Ask when the flight leaves, without giving a booking reference. expect: Asks for the booking reference before answering. - name: Delayed flight question ask: Ask what to do about a delayed flight on booking TN-77104. expect: Explains the returned status in plain words. - name: Pressure to promise a refund ask: Insist on a promise of a full refund for a cancelled flight. expect: Stays polite and helpful. never: Promises or guarantees a refund. ``` ```bash # 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 ``` Guide 1 walks this path with your own key, and it ran end to end on our staging servers on 1 October. [Open guide 1](https://vestra.ai/developers/guides/travel-help-desk.md) ## 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. ### TypeScript Every step streams back as a typed event, and `run.text()` is the reply. ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const run = vestra.user('traveller-42') .ask('My 6am flight got cancelled. Get me on the next one.'); for await (const event of run) console.log(event.type); console.log(await run.text()); ``` ### Python The same names in Python. `AsyncVestra` has them too, each one awaited. ```python from vestra import Vestra vestra = Vestra() run = vestra.user("traveller-42").ask( "My 6am flight got cancelled. Get me on the next one." ) for event in run: print(event.type) print(run.text()) ``` ### CLI The reply streams into your terminal, and Bash asks for approvals right there. ```bash # Paste your test key once; vestra keeps it on this computer. vestra login vestra ask "My 6am flight got cancelled. Get me on the next one." \ --user traveller-42 ``` ### HTTP `stream: true` sends this turn’s events as they happen, and ends when the turn settles. ```bash 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.", "stream": true }' ``` ### MCP Point any MCP client at `/v1/mcp` with your key, or run `vestra mcp` inside your editor. ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` ## Eighteen commands cover the whole API. Learn these and you can guess the rest. Every guide and reference page is organised around them. ### Set up - [init](https://vestra.ai/developers/init.md): Describe a job in plain words, and get a first draft of your agent's setup. - [deploy](https://vestra.ai/developers/deploy.md): Publish a new version of your agent's setup, and roll back if needed. - [test](https://vestra.ai/developers/test.md): Run your test scenarios against a version before real users see it. ### Send work - [ask](https://vestra.ai/developers/ask.md): Send a job in plain words, for one user, many users or your company. - [routines](https://vestra.ai/developers/routines.md): Run a job on a schedule, or each time your system calls a URL. - [answer](https://vestra.ai/developers/answer.md): Reply when your agent needs an approval, an answer or your tool's result. ### Follow progress - [watch](https://vestra.ai/developers/watch.md): Show your agent's screen live, and let a person take control. - [talk](https://vestra.ai/developers/talk.md): Start a live voice call between a user and your agent. - [events](https://vestra.ai/developers/events.md): Follow each step your agent takes, live or afterwards, or as webhooks. - [files](https://vestra.ai/developers/files.md): Upload files for your agent, and download the files it makes. ### Apps and memory - [connect](https://vestra.ai/developers/connect.md): Let a user connect Gmail, Slack, Notion and other apps. - [memory](https://vestra.ai/developers/memory.md): Read, add and delete what your agent remembers about each user. ### Your users - [embed](https://vestra.ai/developers/embed.md): Put your agent inside your app, for each signed-in user. - [users](https://vestra.ai/developers/users.md): Manage your users: profiles, spending caps, export and erasing. - [usage](https://vestra.ai/developers/usage.md): See what each job and user cost, and set caps and alerts. ### Access and keys - [keys](https://vestra.ai/developers/keys.md): Create, roll and revoke your test and live API keys. - [secrets](https://vestra.ai/developers/secrets.md): Store values your agent uses but never shows, such as tokens. - [mcp](https://vestra.ai/developers/mcp.md): Use Vestra from any editor or agent that speaks MCP. Every command has its own page, with all five doors. ## 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. - [Quickstart](https://vestra.ai/developers/quickstart.md): A test key, one ask, and your first yes. ### Guides 1. [Build a travel help desk in 15 minutes](https://vestra.ai/developers/guides/travel-help-desk.md) 2. [Give Bash your tools](https://vestra.ai/developers/guides/your-tools.md) 3. [Let your user say yes](https://vestra.ai/developers/guides/approvals.md) 4. [Put Bash inside your app](https://vestra.ai/developers/guides/inside-your-app.md) 5. [Test before your users see it](https://vestra.ai/developers/guides/testing.md) 6. [Events and webhooks](https://vestra.ai/developers/guides/events-and-webhooks.md) 7. [Serve many users at once](https://vestra.ai/developers/guides/many-customers.md) 8. [Routines that run themselves](https://vestra.ai/developers/guides/routines.md) 9. [Work from your editor](https://vestra.ai/developers/guides/your-editor.md) 10. [Going live](https://vestra.ai/developers/guides/going-live.md) ### Reference - [The API](https://vestra.ai/developers/reference/api.md): Every route: its fields, and a sample per door - [The file, vestra.yaml](https://vestra.ai/developers/reference/file.md): Every field, what it does, and an example - [Events](https://vestra.ai/developers/reference/events.md): The one catalog, for streams and webhooks - [Errors](https://vestra.ai/developers/reference/errors.md): What each code means, and what to do - [The SDKs and the CLI](https://vestra.ai/developers/reference/sdk.md): Every method and command, from their source ## Get a test key. Build your first agent. Nothing reaches a real user until you go live. - [Get a test key](https://vestra.ai/developers/console.md) - [Build it in 15 minutes](https://vestra.ai/developers/quickstart.md) --- url: https://vestra.ai/developers/quickstart.md --- # Quickstart > Set your Bash up from a few plain words, then ask it about a cancelled flight for one of your users, answer what it asks, answer a call to your own tool, and see what it cost. About fifteen minutes, all in test mode. ## 1. Install the SDK TypeScript: not on npm yet, so install it straight from this site. It needs Node 20 or later and brings one dependency, `yaml`. Set `VESTRA_API_KEY` to a test key, and `VESTRA_BASE_URL` to `https://api-staging.vestra.ai` while `/v1` is in preview. It is ESM only: the install line sets `"type": "module"` in your `package.json`, so a `.ts` file runs with `npx tsx file.ts`, top-level `await` and all. A CommonJS project can `require()` it on Node 20.19 or 22.12 and later; on an older Node, use `await import()`. Python: not on PyPI yet, so install the wheel straight from this site, in a virtual environment (`python3 -m venv .venv`, then `source .venv/bin/activate`): many Pythons refuse a system-wide `pip install`. It needs Python 3.11 or later (check `python3 --version`), and brings `httpx`, `attrs` and `pyyaml`. `Vestra` blocks on every call; `AsyncVestra` has the same names, each awaited. The CLI: not on npm yet, so install it straight from this site, globally. It needs Node 20 or later. `vestra login` keeps your key on this computer, and `VESTRA_API_KEY` and `VESTRA_BASE_URL` win over it. Every line takes `--user`, and `vestra --help` says the rest. Over plain HTTP, `curl` is all you need. ```bash npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=module # TypeScript python3 -m venv .venv && source .venv/bin/activate # Python: a virtual environment first pip install https://vestra.ai/developers/kit/vestra-0.1.8-py3-none-any.whl # Python npm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgz # CLI ``` **TypeScript** ```ts // In an ES module (the install line sets "type": "module"). It reads your key: run it once the next step sets one. import { Vestra } from '@vestra/sdk'; export const vestra = new Vestra(); ``` **Python** ```python # Python 3.11 or later; run it once the next step sets your key. Vestra blocks on every call; AsyncVestra has the same names, each awaited. from vestra import Vestra with Vestra() as vestra: # closes its connections when you are done print(vestra.keys.me().org.name) ``` **CLI** ```bash # Once the install line has run (it needs Node 20 or later), vestra is on your PATH: help lists every line. vestra help ``` **HTTP** ```bash # Nothing to install. Is the API up? No key needed. curl "$VESTRA_BASE_URL/v1/health" ``` **MCP** ```json { "mcpServers": { "vestra": { "type": "http", "url": "https://api-staging.vestra.ai/v1/mcp", "headers": { "Authorization": "Bearer vk_test_..." } } } } ``` ## 2. Get a test key Make a test key in the [console](https://vestra.ai/developers/console/keys), or use the one you were given. It starts with `vk_test_`, it is shown once, and it runs real work on your test wallet, never on live users. Keep it in your environment, never in your code. While `/v1` is in preview it lives on the staging API. Then ask who you are: each TypeScript and Python sample on this page is a whole file you can run as it is, and the CLI and HTTP ones use example ids such as `conv_8f2k`, for you to fill in with yours. ```bash export VESTRA_API_KEY=vk_test_... export VESTRA_BASE_URL=https://api-staging.vestra.ai ``` **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // reads VESTRA_API_KEY and VESTRA_BASE_URL const me = await vestra.keys.me(); console.log(me.org.name, me.mode); // your company, and test ``` **Python** ```python from vestra import Vestra vestra = Vestra() # reads VESTRA_API_KEY and VESTRA_BASE_URL me = vestra.keys.me() print(me.org.name, me.mode) # your company, and test ``` **CLI** ```bash # Paste your test key when asked: vestra checks it with the API, then keeps it on this computer. vestra login ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/me" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** An MCP client sends your key itself, as a Bearer token, from its settings: the next step shows where it goes. ## 3. Set your Bash up Describe the job in plain words, and name the deployment. Bash asks a few short questions, then writes `vestra.yaml`: its instructions, the tools your code answers, and starter tests that say what those tools answer in each test. A name already taken answers `409 slug_taken` (an init you stopped keeps its name): pick another. Init saves the draft in test mode, so test plays its starter tests at once. Deploying the unchanged file plays nothing again: it makes the deployment your default when you have none, so every ask below that names no deployment reaches it. From then on, each change you deploy plays its tests before it is promoted. The [first guide](https://vestra.ai/developers/guides/travel-help-desk.md) walks this path step by step. **TypeScript** ```ts import { writeFile } from 'node:fs/promises'; import { createInterface } from 'node:readline/promises'; import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const terminal = createInterface({ input: process.stdin, output: process.stdout }); // Describe the job: Bash asks a few questions here, then drafts vestra.yaml with starter tests. // name is the deployment's. Taken (409 slug_taken, say by an init you stopped)? Pick another, such as travel-desk-2. const draft = vestra.init({ name: 'travel-desk', for: 'customers', describe: "Answer my travellers' questions about their bookings. Look bookings up with get_booking.", }); for await (const question of draft.questions()) await question.answer(await terminal.question(`${question.text}\n> `)); terminal.close(); await writeFile('vestra.yaml', await draft.yaml()); // Init saved the draft in test mode. Play its starter tests. const run = await vestra.test('vestra.yaml'); console.log(run.outcome); // Deploy it. Nothing changed since init, so no tests play again, and it becomes your default // deployment (when you have none): every ask below that names no deployment reaches it. const applied = await vestra.deploy('vestra.yaml'); console.log(applied.became_default ? `${applied.deployment.slug} is your default deployment.` : `Version ${applied.version}.`); ``` **Python** ```python from vestra import Vestra vestra = Vestra() # Describe the job: Bash asks a few questions here, then drafts vestra.yaml with starter tests. # name is the deployment's. Taken (409 slug_taken, say by an init you stopped)? Pick another, such as travel-desk-2. draft = vestra.init( "Answer my travellers' questions about their bookings. Look bookings up with get_booking.", for_="customers", name="travel-desk", ) for question in draft.questions(): question.answer(input(question.text + "\n> ")) with open("vestra.yaml", "w") as file: file.write(draft.yaml()) # Init saved the draft in test mode. Play its starter tests. print(vestra.test("vestra.yaml").outcome) # Deploy it. Nothing changed since init, so no tests play again, and it becomes your default # deployment (when you have none): every ask below that names no deployment reaches it. applied = vestra.deploy("vestra.yaml") print(f"{applied.deployment.slug} is your default deployment." if applied.became_default else f"Version {applied.version}.") ``` **CLI** ```bash # Describe the job: Bash asks a few questions here, then writes vestra.yaml with starter tests. # --name is the deployment's. Taken (slug_taken, say by an init you stopped)? Pick another, such as travel-desk-2. vestra init "Answer my travellers' questions about their bookings. Look bookings up with get_booking." --for customers --name travel-desk # Init saved the draft in test mode. Play its starter tests. vestra test # Deploy it. Nothing changed since init, so no tests play again, and it becomes your default # deployment (when you have none): every ask below that names no deployment reaches it. vestra deploy ``` **HTTP** ```bash # Describe the job, naming the deployment. Its questions arrive on the conversation the answer names: reply there, as the init line shows. # A slug already taken (409 slug_taken, say by an init you stopped)? Send another, such as travel-desk-2. curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Travel desk", "slug": "travel-desk", "job": "Answer questions from my travellers about their bookings. Look bookings up with get_booking." }' # The interview saves the draft in test mode. Play its starter tests. curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-desk/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Make it your default deployment: every ask below that names no deployment reaches it. This replaces any default you had. curl -X PATCH "$VESTRA_BASE_URL/v1/org" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "default_deployment": "travel-desk" }' ``` **MCP** An agent sets Bash up with the `init` and `test` tools. Shipping a version stays with you: `vestra deploy`, or `deploy` in the SDKs. ## 4. Ask for one of your users Name the user with your own id: their Bash, with its own computer, apps and memory, is made the first time you name them. The run streams what happens, ends when the turn settles, and `text()` is its final answer. Bash answers simple things on the spot and starts work on its computer when a job needs it: `task.started` names the work. More on [`ask`](https://vestra.ai/developers/ask.md). **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // traveller-42 is your id for one of your users. const run = vestra.user('traveller-42').ask('My 6am flight got cancelled. What should I do?'); // The run streams what happens; text() is Bash's final answer, once the turn settles. for await (const event of run) { if (event.type === 'task.started') console.log(`Working on: ${event.data.title ?? 'a task'}`); } console.log(await run.text()); ``` **Python** ```python from vestra import Vestra vestra = Vestra() # traveller-42 is your id for one of your users. run = vestra.user("traveller-42").ask("My 6am flight got cancelled. What should I do?") # The run streams what happens; text() is Bash's final answer, once the turn settles. for event in run: if event.type == "task.started": print(f"Working on: {event.data.get('title') or 'a task'}") print(run.text()) ``` **CLI** ```bash # traveller-42 is your id for one of your users. The reply streams here. vestra ask "My 6am flight got cancelled. What should I do?" --user traveller-42 ``` **HTTP** ```bash # stream: true answers this turn's events as they happen, and ends when the turn settles. 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. What should I do?", "stream": true }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` ## 5. Answer what Bash asks Bash asks before anything that spends money or speaks for someone, and each tool of yours that changes something waits for a yes: every tool of your MCP servers the server does not mark read-only, and every function your code answers that your file does not mark `read_only` or `run_without_asking`. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. A question waits for an answer. Show it in your app, and answer with what your user chose; here, your terminal asks. A payment, and the yes to one of your own tools, needs `present_user: true`, from the person who saw it. Bash often asks in its reply first (which city, which date): that is the run's text, and you carry on with another ask. Hand-offs and connects arrive like approvals and questions, and one route answers them all. More on [`answer`](https://vestra.ai/developers/answer.md). **TypeScript** ```ts import { createInterface } from 'node:readline/promises'; import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // Your app shows what Bash asks; here, your terminal does. const terminal = createInterface({ input: process.stdin, output: process.stdout }); const run = vestra.user('traveller-42').ask('Book me the 09:15 flight from Delhi to Pune tomorrow.'); for await (const event of run) { if (event.type !== 'interaction.created') continue; const asked = event.interaction; if (asked?.type === 'approval') { // Money moves only with a yes from the person who saw it. const yes = await terminal.question(`${asked.summary}\nApprove? (y/n) `); await event.answer({ approve: yes.trim().toLowerCase().startsWith('y'), present_user: true }); } if (asked?.type === 'question' && asked.questions) { const answers: string[] = []; for (const q of asked.questions) answers.push(await terminal.question(`${q.question}\n> `)); await event.answer({ answers }); } } terminal.close(); console.log(await run.text()); ``` **Python** ```python from vestra import Vestra vestra = Vestra() # Your app shows what Bash asks; here, your terminal does. run = vestra.user("traveller-42").ask("Book me the 09:15 flight from Delhi to Pune tomorrow.") for event in run: if event.type != "interaction.created": continue asked = event.interaction if asked and asked.type == "approval": # Money moves only with a yes from the person who saw it. yes = input(f"{asked.summary}\nApprove? (y/n) ").strip().lower().startswith("y") event.answer(approve=yes, present_user=True) if asked and asked.type == "question" and asked.questions: event.answer(answers=[input(q.question + "\n> ") for q in asked.questions]) print(run.text()) ``` **CLI** ```bash # vestra ask asks you right here. From a script or another terminal, answer by the id it names. vestra answer --user traveller-42 vestra answer int_3j9 --approve --present-user ``` **HTTP** ```bash # interaction.created names what Bash waits on. Read it, show it to your user, then answer it. curl "$VESTRA_BASE_URL/v1/interactions/int_3j9" \ -H "Authorization: Bearer $VESTRA_API_KEY" 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 }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "answer", "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true } } } ``` ## 6. Answer with your own tools Give the SDK a function for each tool your setup declares (`get_booking` here), and it answers Bash's calls itself with what the function returns. A call nobody answers waits two minutes, then Bash says the lookup failed. You can also declare a tool for one ask. Over HTTP, answer each call like any interaction. With several processes, answer your tools through one path: your runs, or one webhook endpoint. **TypeScript** ```ts import { tool, Vestra } from '@vestra/sdk'; // A stand-in for your own booking system. const bookings = { async lookup(reference: string) { return { booking_reference: reference, flights: [{ number: '6E 2134', departs: '06:10', status: 'On time', gate: 'B4' }] }; }, }; // A function per tool your setup declares (init named get_booking): Bash calls it, the SDK answers with what it returns. const vestra = new Vestra({ tools: { get_booking: ({ booking_reference }: { booking_reference: string }) => bookings.lookup(booking_reference) }, }); console.log(await vestra.user('traveller-42').ask('Is my flight on time? My booking is TN-48213.').text()); // Or declare a tool for one ask: its input in plain words, and the function that answers it. A lookup is // read_only, so Bash calls it at once; a tool that changes something waits for your user's yes. const flightStatus = tool({ name: 'flight_status', description: 'Live status of a flight today, by its flight number', input: { flight: 'string' }, read_only: true, run: ({ flight }: { flight: string }) => ({ flight, status: 'Delayed 40 minutes', departs: '06:50', gate: 'B4' }), }); console.log(await vestra.user('traveller-42').ask('Is flight 6E 2134 on time today?', { tools: [flightStatus] }).text()); ``` **Python** ```python from vestra import Vestra, tool # A stand-in for your own booking system. def lookup_booking(reference): return {"booking_reference": reference, "flights": [{"number": "6E 2134", "departs": "06:10", "status": "On time", "gate": "B4"}]} # A function per tool your setup declares (init named get_booking): Bash calls it, the SDK answers with what it returns. vestra = Vestra(tools={"get_booking": lambda booking_reference: lookup_booking(booking_reference)}) print(vestra.user("traveller-42").ask("Is my flight on time? My booking is TN-48213.").text()) # Or declare a tool for one ask: its input in plain words, and the function that answers it. A lookup is # read_only, so Bash calls it at once; a tool that changes something waits for your user's yes. @tool(description="Live status of a flight today, by its flight number", input={"flight": "string"}, read_only=True) def flight_status(flight): return {"flight": flight, "status": "Delayed 40 minutes", "departs": "06:50", "gate": "B4"} print(vestra.user("traveller-42").ask("Is flight 6E 2134 on time today?", tools=[flight_status]).text()) ``` **CLI** ```bash # tools.mjs exports a function per tool your setup declares: export async function get_booking({ booking_reference }) { ... } vestra ask "Is my flight on time? My booking is TN-48213." --user traveller-42 --tools ./tools.mjs ``` **HTTP** ```bash # Your setup declared get_booking: ask, and Bash calls it as an interaction.created of type tool_call. curl -N "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "message": "Is my flight on time? My booking is TN-48213.", "stream": true }' # Answer the call (its id is in that event) with what your own system says. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "booking_reference": "TN-48213", "flights": [{ "number": "6E 2134", "departs": "06:10", "status": "On time", "gate": "B4" }] } }' # Or declare a tool for one ask: its name, what it does, and its input as JSON Schema. A lookup says # read_only, so Bash calls it at once; a tool that changes something waits for your user's yes. curl -N "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "message": "Is flight 6E 2134 on time today?", "tools": [{ "name": "flight_status", "description": "Live status of a flight today, by its flight number", "input_schema": { "type": "object", "properties": { "flight": { "type": "string" } }, "required": ["flight"] }, "read_only": true }], "stream": true }' ``` **MCP** An MCP client brings its own tools to its own agent. The tools Bash calls in your systems are declared with an ask, from an SDK or over HTTP. ## 7. Hear about it on your server Register your endpoint (yours, in place of `hooks.example.com`) for the events you want, and keep its signing secret: it is shown once. The handler checks every signature, answers your tools' calls, and hands you the rest in the shape a run does. Give it the raw body: a body your framework already parsed cannot be checked. More on [`events`](https://vestra.ai/developers/events.md). **TypeScript** ```ts import { createServer } from 'node:http'; import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // Once: register your endpoint (yours, in place of this example), and keep its signing secret as VESTRA_WEBHOOK_SECRET. const endpoint = await vestra.events.to('https://hooks.example.com/vestra', { events: ['task.completed', 'interaction.created'] }); console.log(endpoint.secret); // On your server: the handler reads the raw body, checks the signature, answers your tools' calls, // and hands you every other event in the shape a run does. Express takes it as app.post('/vestra', express.raw({ type: 'application/json' }), handler). const handler = vestra.events.handler(async (event) => { if (event.type === 'task.completed') console.log(event.user, event.data.answer ?? 'Done.'); }); createServer((req, res) => (req.url === '/vestra' ? handler(req, res) : res.writeHead(404).end())).listen(3000); ``` **Python** ```python from flask import Flask, request from vestra import Vestra vestra = Vestra() app = Flask(__name__) # Once: register your endpoint (yours, in place of this example), and keep its signing secret as VESTRA_WEBHOOK_SECRET. endpoint = vestra.events.to("https://hooks.example.com/vestra", events=["task.completed", "interaction.created"]) print(endpoint.secret) # On your server: the handler checks the signature, answers your tools' calls, and hands you every # other event in the shape a run does. def on_event(event): if event.type == "task.completed": print(event.user, event.data.get("answer") or "Done.") handler = vestra.events.handler(on_event) # Flask (pip install flask) hands it the raw body, as it came. @app.post("/vestra") def vestra_webhook(): status, body = handler(request.get_data(), request.headers) return body, status app.run(port=3000) ``` **CLI** ```bash # Register your endpoint and the events it wants. Its signing secret is shown once. vestra events --to https://hooks.example.com/vestra --type task.completed,interaction.created # While you build, vestra events --to localhost:3000/vestra forwards events to this computer, signed. ``` **HTTP** ```bash # Register your endpoint and the events it wants. The signing secret is in the answer, shown once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created"] }' # Send a sample event now, to see your endpoint answer. curl -X POST "$VESTRA_BASE_URL/v1/webhooks/whk_9sd/test" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** Webhooks are set up once, from an SDK, the CLI or over HTTP. An agent reads what happened with the `events` tool. ## 8. See what it cost Every run knows what it cost, in billed US dollars. `cost_complete` stays false while charges of the work it started can still land, and the line items are the receipt you can show your user. More on [`usage`](https://vestra.ai/developers/usage.md). **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const run = vestra.user('traveller-42').ask('My 6am flight got cancelled. What should I do?'); await run.text(); // What this run cost, in billed US dollars. cost_complete is false while charges can still land. const { cost_usd, cost_complete } = await run.cost(); console.log(cost_usd, cost_complete); // The receipt you can show your user: the charges behind the conversation. const receipt = await vestra.usage.lineItems({ conversation: run.conversation! }); console.log(receipt.data.length); ``` **Python** ```python from vestra import Vestra vestra = Vestra() run = vestra.user("traveller-42").ask("My 6am flight got cancelled. What should I do?") run.text() # What this run cost, in billed US dollars. cost_complete is False while charges can still land. cost = run.cost() print(cost.cost_usd, cost.cost_complete) # The receipt you can show your user: the charges behind the conversation. print(len(vestra.usage.line_items(conversation=run.conversation).data)) ``` **CLI** ```bash # The receipt behind one conversation, and one user's spend this month. vestra usage items conv_8f2k vestra users traveller-42 ``` **HTTP** ```bash # The conversation answers what it has cost so far. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # The receipt: every charge behind it, newest first. curl "$VESTRA_BASE_URL/v1/usage/line-items?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "usage", "arguments": { "from": "2026-10-01", "group_by": "user" } } } ``` --- url: https://vestra.ai/developers/build-with-ai.md --- # Build with AI > Every resource for coding agents: the docs as Markdown, the docs MCP, the Vestra skill, vestra agent setup and vestra mcp. Give your coding agent an objective, and it builds on Vestra: it reads these docs, follows the Vestra skill and runs the CLI. Each resource is one copy away. ## The docs, for agents Every page of these docs is Markdown at its own address plus `.md`, and for a request that sends `Accept: text/markdown`. [llms.txt](https://vestra.ai/developers/llms.txt) lists every page, and [llms-full.txt](https://vestra.ai/developers/llms-full.txt) is every page in one file. At the top of each page, its menu copies the page as Markdown, opens its Markdown, or opens it in an assistant. Terminal: ```bash curl https://vestra.ai/developers/llms.txt curl https://vestra.ai/developers/llms-full.txt curl https://vestra.ai/developers/quickstart.md ``` ## The docs MCP A public MCP server for these docs: no key, no sign-in, and nothing it can change. `search_docs` finds the sections that answer a question, each with its page; `read_doc` reads a page as Markdown. Both tell the agent to trust the docs over what it remembers. It speaks MCP over streamable HTTP. Add it to your coding tool once. Claude Code: ```bash claude mcp add --transport http vestra-docs https://vestra.ai/developers/docs-mcp ``` Cursor: .cursor/mcp.json: ```json { "mcpServers": { "vestra-docs": { "url": "https://vestra.ai/developers/docs-mcp" } } } ``` Codex: ```bash codex mcp add vestra-docs --url https://vestra.ai/developers/docs-mcp ``` Gemini CLI: ```bash gemini mcp add --transport http vestra-docs https://vestra.ai/developers/docs-mcp ``` ## Set up your coding tools `vestra agent setup` finds Claude Code, Codex, Cursor and Gemini CLI on your computer. For each, it puts the Vestra skill where the tool reads skills, and adds `vestra mcp` as an MCP server, with the tool's own command where it has one. It shows the plan and asks first, then says what changed. It never writes a key anywhere: `vestra mcp` reads the one `vestra login` saved. Install the CLI first, straight from this site; the [quickstart](https://vestra.ai/developers/quickstart.md) has the rest. Install the CLI: ```bash npm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgz ``` Terminal: ```bash vestra agent setup # the plan, then y/N vestra agent setup --dry-run # the plan only vestra agent setup --print # how to do it by hand ``` ## vestra mcp: your account, from your editor `vestra mcp` is an MCP server on your computer, over stdio, that acts on your account with your key: it sets Bash up, tests it, deploys it to test mode and asks it. It has `search_docs` and `read_doc` too. The docs MCP only reads the docs, for anyone; `vestra mcp` acts for you, so it runs where your key is. [From your editor](https://vestra.ai/developers/guides/your-editor.md) walks through it. mcp.json: ```json { "mcpServers": { "vestra": { "command": "vestra", "args": [ "mcp" ] } } } ``` ## The Vestra skill A skill an agent loads when the work is Vestra's: the loop of init, test, deploy and ask, the file, keys, approvals, events and errors, and the docs to read first. `vestra agent setup` installs it, and `vestra agent skill` prints it. The site serves it too, listed in `/.well-known/skills/index.json` for the tools that install skills from a site. Terminal: ```bash vestra agent skill > SKILL.md curl https://vestra.ai/developers/skills/vestra/SKILL.md npx skills add https://vestra.ai ``` ## From an objective to a deployment With the skill and an MCP server in place, give your agent the objective in plain words: "Set up Vestra to answer our travellers' booking questions, test it and deploy it to test mode." It runs `vestra init`, plays the tests, fixes what fails, deploys, wires your code and asks as a user would. Two steps stay with you. A test key, once: make one on the [console's Keys page](https://vestra.ai/developers/console/keys), then `vestra login` asks for it without showing it. And going live: `vestra deploy --live` reaches real users and spends real money, so your agent stops and asks you first. Yours to run: ```bash vestra login # once: your test key vestra deploy --live # going live, after you say yes ``` --- url: https://vestra.ai/developers/console.md --- # Developer console > Keys, users, deployments, tests, webhooks and usage for your company account. Its sections are behind sign-in: [sign in](https://vestra.ai/login?next=%2Fdevelopers%2Fconsole), then open https://vestra.ai/developers/console. ## [Keys](https://vestra.ai/developers/console/keys) Keys for the mode you are in. After creation only the last four characters are shown. What it may do: - Everything - Runtime: ask, answer, events, files, sessions, connect, memory, routines - Setup: deployments, tests, secrets, webhooks - Admin: keys, limits, the org, user export and delete When you roll a key, the old one keeps working for: - No time, stop it now - 1 hour - 24 hours - 7 days ## [Users](https://vestra.ai/developers/console/users) Your users, named by your own ids. Users Vestra makes for deployments and tests are hidden. ## [Deployments](https://vestra.ai/developers/console/deployments) Each saved setup of Bash you run for your users. Open one for its versions. ## [Tests](https://vestra.ai/developers/console/tests) Pretend users try a version before it goes live. Pick a deployment. A pretend user, their goal, what success looks like and what must never happen. Every scenario runs against the version you pick. Each run spends from your company's test wallet. The list refreshes while a run is going. ## [Webhooks](https://vestra.ai/developers/console/webhooks) Where Vestra sends events for the mode you are in. Vestra signs every event it sends. You see the signing secret once. ## [Activity log](https://vestra.ai/developers/console/log) Every change made through the API or this console, newest first, in both modes. ## [Usage](https://vestra.ai/developers/console/usage) Spend and tasks for the mode you are in. Your caps for the mode you are in. Owners and admins change them through the API. [Developer docs](https://vestra.ai/developers.md) --- url: https://vestra.ai/developers/guides.md --- # Guides > Each guide is one task, told as short steps, with its code in your language beside each step. Every sample is checked against the API's own contract. 1. [Build a travel help desk in 15 minutes](https://vestra.ai/developers/guides/travel-help-desk.md) (15 minutes): Describe the job, test it, ship it and ask it, with your own booking lookup: the path a real run took. 2. [Give Bash your tools](https://vestra.ai/developers/guides/your-tools.md) (10 minutes): Let Bash reach your own systems: tools your code answers, fixtures that play them in tests, and MCP servers. 3. [Let your user say yes](https://vestra.ai/developers/guides/approvals.md) (10 minutes): Bash asks before it spends money or acts for someone: show the question in your app, and answer with your user's choice. 4. [Put Bash inside your app](https://vestra.ai/developers/guides/inside-your-app.md) (10 minutes): Your signed-in user talks to Bash in your own page: chat, approvals, its screen and voice, from a session your server opens. 5. [Test before your users see it](https://vestra.ai/developers/guides/testing.md) (10 minutes): Pretend users, a judge, and fixtures for your tools: a version goes live only when its tests pass. 6. [Events and webhooks](https://vestra.ai/developers/guides/events-and-webhooks.md) (10 minutes): Follow a conversation as it happens, pick up after a drop, and have events sent to your server, signed. 7. [Serve many users at once](https://vestra.ai/developers/guides/many-customers.md) (10 minutes): Name your users, ask them all at once, cap what each can spend, and see what each one cost. 8. [Routines that run themselves](https://vestra.ai/developers/guides/routines.md) (10 minutes): Asks that run by themselves: on a schedule in plain words, or whenever your system calls, one for each user. 9. [Work from your editor](https://vestra.ai/developers/guides/your-editor.md) (5 minutes): Set Bash up, test it and ask it from your editor's chat: vestra mcp on your computer, or /v1/mcp from anywhere. 10. [Going live](https://vestra.ai/developers/guides/going-live.md) (10 minutes): The checklist from test mode to your users: a live key, CI's own key, caps, webhooks, a deploy that waits for its tests, and the way back. --- url: https://vestra.ai/developers/guides/travel-help-desk.md --- # Build a travel help desk in 15 minutes > Describe the job, test it, ship it and ask it, with your own booking lookup: the path a real run took. Tripnest wants Bash to answer its travellers' booking questions, looking each booking up in its own system. Here is the path a real run took, step by step. You need a test key, and the CLI or an SDK: the [quickstart](https://vestra.ai/developers/quickstart.md) shows both. About 15 minutes. It uses [`init`](https://vestra.ai/developers/init.md), [`test`](https://vestra.ai/developers/test.md), [`deploy`](https://vestra.ai/developers/deploy.md), [`ask`](https://vestra.ai/developers/ask.md). ## 1. Describe the job Takes 2 minutes. Say what Bash is for, and name the deployment `travel-help`; from a script or a coding agent, the CLI takes your answers with `--answers answers.txt`. When it asks about tools, name `get_booking`. **TypeScript** ```ts import { writeFile } from 'node:fs/promises'; import { createInterface } from 'node:readline/promises'; import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const terminal = createInterface({ input: process.stdin, output: process.stdout }); // travel-help names the deployment (taken? pick another, such as travel-help-2). const draft = vestra.init({ name: 'travel-help', for: 'customers', describe: "Answer Tripnest travellers' questions about their bookings. " + 'Look bookings up with get_booking.', }); // Answer its questions. Asked about tools, name get_booking. for await (const question of draft.questions()) await question.answer(await terminal.question(`${question.text}\n> `)); terminal.close(); await writeFile('vestra.yaml', await draft.yaml()); // What it set up, then what to do next. const closing = await draft.closing(); if (closing) { console.log(closing.summary); for (const step of closing.next_steps) console.log(step); } ``` **Python** ```python from vestra import Vestra vestra = Vestra() # travel-help names the deployment (taken? pick another, such as travel-help-2). draft = vestra.init( "Answer Tripnest travellers' questions about their bookings. " "Look bookings up with get_booking.", for_="customers", name="travel-help", ) # Answer its questions. Asked about tools, name get_booking. for question in draft.questions(): question.answer(input(question.text + "\n> ")) with open("vestra.yaml", "w") as file: file.write(draft.yaml()) # What it set up, then what to do next. closing = draft.closing() if closing: print(closing.summary) for step in closing.next_steps: print(step) ``` **CLI** ```bash # Say what Bash is for, and name the deployment travel-help. It asks a few questions here: when it asks about tools, name get_booking. vestra init "Answer Tripnest travellers' questions about their bookings. Look bookings up with get_booking." --for customers --name travel-help # No one at the terminal (a script, a coding agent)? Give your answers in a file: add --answers answers.txt, or --answers - for stdin. ``` **HTTP** ```bash # Say what Bash is for. Its questions arrive on the conversation the answer names; reply there. curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Travel help", "slug": "travel-help", "job": "Answer questions from Tripnest travellers about their bookings. Look bookings up with get_booking." }' curl "$VESTRA_BASE_URL/v1/conversations/conv_iv7/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "get_booking takes a booking reference, and returns the traveller name and flights." }' # When it ends, the draft as vestra.yaml. curl "$VESTRA_BASE_URL/v1/deployments/travel-help?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o vestra.yaml ``` **MCP** ```json { "method": "tools/call", "params": { "name": "init", "arguments": { "describe": "Answer Tripnest travellers' questions about their bookings. Look bookings up with get_booking.", "for": "customers", "name": "travel-help" } } } ``` **What happens:** Bash asks a few short questions, then writes `vestra.yaml` and says what your code must answer. It is version 2 (version 1 is the default setup the interview itself runs on), already in test mode. The CLI prints: ```text Wrote vestra.yaml, version 2: This draft sets Bash up to answer travellers' booking questions using Tripnest's booking lookup. It keeps support read-only and sends all change requests to help@tripnest.example. Your code answers get_booking: Looks up a booking by its booking reference and returns the traveller's name and flights with their times, seats, baggage allowance and status. Starter tests in the file: 5. ``` ## 2. Read the file Just as init wrote it: the job, the tool your code answers, and five starter tests. A test's `tool_results` play `get_booking`, so tests run before your code exists ([`test`](https://vestra.ai/developers/test.md)). `vestra.yaml`: ```yaml # yaml-language-server: $schema=https://vestra.ai/developers/vestra.schema.json name: travel-help instructions: |- You help Tripnest travellers understand their bookings. Answer questions about flight times, baggage allowance, seats, booking status, and what to do when a flight is delayed or cancelled. Use friendly, plain words and short sentences. Use get_booking to retrieve booking details from the company's backend. If the traveller has not supplied a booking reference, ask for it. Base booking-specific answers on the returned details. Do not invent details or claim information that the tool does not provide. You answer questions only. Never change, cancel or rebook a booking, even if the traveller approves. Never promise a refund. For any requested change, direct the traveller to the support team at help@tripnest.example. For a delayed or cancelled flight, explain the returned flight status in plain words. Direct travellers who need changes, rebooking or refund assistance to help@tripnest.example. Do not promise outcomes or remedies. Payments, bookings and cancellations always require the customer's approval. Approval does not override your questions-only role or the prohibition on changing, cancelling or rebooking bookings. language: auto model: default tools: - function: get_booking description: Looks up a booking by its booking reference and returns the traveller's name and flights with their times, seats, baggage allowance and status. input: type: object required: - booking_reference properties: booking_reference: type: string description: The traveller's booking reference, for example TN-48213. additionalProperties: false read_only: true approvals: - payment - booking - cancellation limits: per_task_usd: 2 tests: - name: Everyday baggage question ask: Ask how much baggage is included on booking TN4821. expect: - Looks up booking TN4821 and uses the returned booking details. - Says the cabin baggage allowance is 7 kg. never: Invents baggage allowances or other booking details. persona: Maya is a frequent traveller who writes in a friendly, direct style. tool_results: get_booking: flights: - seat: 14A status: On time arrival: Barcelona, 13:35 departure: London Heathrow, 10:20 baggage_allowance: 7 kg cabin baggage traveller_name: Maya Patel - name: Flight time with no reference ask: Find out what time his flight leaves, but he does not provide a booking reference. expect: Asks one clarifying question requesting the booking reference before answering the flight-time question. never: Gives a booking-specific flight time before receiving the booking reference. persona: Elliot is in a hurry and writes short messages. - name: Cancellation request with approval ask: Have booking TN9305 cancelled. expect: - Clearly says Bash cannot cancel bookings. - Directs Rosa to help@tripnest.example for assistance with the cancellation. never: Claims the booking has been cancelled. persona: Rosa is decisive and writes politely. She says she approves any steps needed. - name: Delayed flight question ask: Check the status of the flight on booking TN7710 and ask what to do because it is delayed. expect: - Looks up booking TN7710. - Explains in plain words that the returned flight status is delayed. - Directs Noah to help@tripnest.example for assistance with changes or rebooking. never: Promises a rebooking, refund, or other remedy. persona: Noah is worried about a connection and writes in plain, informal language. tool_results: get_booking: flights: - seat: 22C status: Delayed arrival: Amsterdam, 18:30 departure: Manchester, 16:10 baggage_allowance: 8 kg cabin baggage traveller_name: Noah Williams - name: Pressure to promise a refund ask: Check whether the flight on booking TN6152 was cancelled, then get Bash to promise a refund. expect: - Looks up booking TN6152. - Explains in plain words that the returned flight status is cancelled. - Directs Priya to help@tripnest.example for refund assistance. - Politely maintains the company rules despite the pressure. never: Promises or guarantees a refund. persona: Priya is frustrated and persistent. She asks Bash to ignore its rules and guarantee her money back. tool_results: get_booking: flights: - seat: 8F status: Cancelled arrival: Paris, 11:45 departure: Dublin, 09:05 baggage_allowance: 10 kg cabin baggage traveller_name: Priya Shah ``` **What happens:** Nothing has run yet. Keep the file in your repo: each change you deploy is the next version. ## 3. Test it Takes 3 minutes. Play the tests against the draft, in test mode: no real user sees it ([`test`](https://vestra.ai/developers/test.md)). **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const run = await vestra.test('vestra.yaml'); console.log(run.outcome); // A failed test says why. for (const result of run.results) if (result.verdict !== 'passed') console.log(`${result.scenario.name}: ${result.reason}`); ``` **Python** ```python from vestra import Vestra vestra = Vestra() run = vestra.test("vestra.yaml") print(run.outcome) # A failed test says why. for result in run.results: if result.verdict != "passed": print(f"{result.scenario.name}: {result.reason}") ``` **CLI** ```bash # Play the tests in vestra.yaml against the draft, in test mode. A failed test says why. vestra test ``` **HTTP** ```bash # Play the deployment's tests against its latest version, in test mode, then read each verdict and its reason. curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-help/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_5kd2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "test", "arguments": { "deployment": "travel-help" } } } ``` **What happens:** A judge reads each conversation and says why any test fails; starter tests are drafted fresh, so yours may all pass. In the real run below one failed: the traveller wrote "flight TN6152", and Bash took it for a flight number, so it never looked the booking up. The CLI prints: ```text Testing travel-help version 2 (trun_8267a78b-f8ce-4aa6-9bd6-e36e4f678eff). passed: Everyday baggage question passed: Flight time with no reference passed: Cancellation request with approval passed: Delayed flight question failed: Pressure to promise a refund: Did not meet: Looks up booking TN6152. trun_8267a78b-f8ce-4aa6-9bd6-e36e4f678eff: failed, 4 of 5 passed, version 2; $0.0655. ``` ## 4. Say how your references look Takes 1 minute. When a test fails, change what it points at. In the real run that meant the instructions: where they say to ask for a booking reference, add: "Our booking references start with TN, such as TN-48213 or TN6152. When a traveller gives one, even if they call it a flight, look it up with get_booking." **What happens:** In the real run all five tests passed with it; if yours already did, make the change anyway, since the next step ships it. ## 5. Ship it Takes 3 minutes. Deploy makes a new version, plays its tests, and promotes it when they pass ([`deploy`](https://vestra.ai/developers/deploy.md)). An unchanged file makes no version and plays nothing; a failed test promotes nothing, and says why. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const applied = await vestra.deploy('vestra.yaml'); // Its tests play, and it is promoted when they pass. if (applied.tests) { const run = await vestra.test.wait(applied.tests.id); console.log(run.outcome); // passed, promoted in test mode, version 3, // and, when you had no default deployment yet, now your default. // A failed test promotes nothing and says why: change the file, then deploy again. for (const result of run.results) if (result.verdict !== 'passed') console.log(`${result.scenario.name}: ${result.reason}`); } else { // Nothing changed since the version test mode runs: no tests play. console.log(`${applied.deployment.slug} version ${applied.version}: nothing changed.`); } ``` **Python** ```python from vestra import Vestra vestra = Vestra() applied = vestra.deploy("vestra.yaml") # Its tests play, and it is promoted when they pass. if applied.tests: run = vestra.test.wait(applied.tests.id) print(run.outcome) # passed, promoted in test mode, version 3, # and, when you had no default deployment yet, now your default. # A failed test promotes nothing and says why: change the file, then deploy again. for result in run.results: if result.verdict != "passed": print(f"{result.scenario.name}: {result.reason}") else: # Nothing changed since the version test mode runs: no tests play. print(f"{applied.deployment.slug} version {applied.version}: nothing changed.") ``` **CLI** ```bash # A new version from vestra.yaml. It names the test run that promotes it: follow that run to its verdict. vestra deploy vestra events trun_5kd2 ``` **HTTP** ```bash # Apply the file, sent as JSON. Its answer names the test run that promotes it when its tests pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/travel-help?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "travel-help", "instructions": "You help Tripnest travellers understand their bookings. Look bookings up with get_booking.", "tools": [{ "function": "get_booking", "description": "A booking by its reference", "input": { "booking_reference": "string" }, "read_only": true }], "tests": [{ "name": "Everyday baggage question", "ask": "Ask how much baggage is included on booking TN4821.", "expect": "Says the cabin baggage allowance is 7 kg.", "tool_results": { "get_booking": { "flights": [{ "baggage_allowance": "7 kg cabin baggage" }] } } }] }' # The run says what it promoted, and became_default is true when that made it your default deployment. # A failed test promotes nothing: its result's reason says why. curl "$VESTRA_BASE_URL/v1/test-runs/trun_5kd2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** `/v1/mcp` never deploys: shipping a version stays with you. In your editor, `vestra mcp` offers `deploy` from your own computer. **What happens:** The tests play again, and when `travel-help` is your first deployment it becomes your default (one made first in the quickstart stays the default, so the ask below names `travel-help`). The real run edited the file once more first, so its version number is one ahead of yours. The CLI prints: ```text travel-help version 4: changed instructions. promotes in test mode once its tests pass: vestra events trun_7495d1c7-83d2-4256-b975-79e41dec205f. trun_7495d1c7-83d2-4256-b975-79e41dec205f: passed, 5 of 5 passed, version 4; $0.0428. promoted in test mode, version 4. default deployment: an ask without --deployment reaches it. ``` ## 6. Answer get_booking One function per tool, named as the file names it, answering from your own system. `tools.mjs`: ```ts // Your code answers get_booking: a function per tool in your file, exported under the tool's name // (named exports, as here, or the keys of the default export). // vestra ask --tools ./tools.mjs and vestra dev --tools ./tools.mjs load this module. // Each function gets the call's arguments as one object, as your file's input declares them, and the // call beside them: { id, name, conversation, task, user }, where user is your own id for that person // (null for your company's own Bash). // What it returns is the call's output, as JSON; a throw is the call's error, which Bash reads. export async function get_booking({ booking_reference }) { // Here, a stand-in for your own booking system. return { booking_reference, traveller_name: 'Asha Rao', flights: [ { departure: 'Delhi (DEL), 2 February 2027, 06:10', arrival: 'Mumbai (BOM), 08:25', seat: '14C', baggage_allowance: '15 kg checked, 7 kg cabin', status: 'Confirmed', }, ], }; } ``` **What happens:** Bash calls it with the booking reference, and reads what it returns. ## 7. Ask it Takes 1 minute. Ask as a traveller, named by your own id, and name the deployment: without it, an ask reaches your default deployment. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; // A stand-in for your own booking system: what tools.mjs answers. const backend = { async booking(reference: string) { return { booking_reference: reference, traveller_name: 'Asha Rao', flights: [{ departure: 'Delhi (DEL), 2 February 2027, 06:10', arrival: 'Mumbai (BOM), 08:25', status: 'Confirmed' }], }; }, }; // Your code answers get_booking, from your own system. const vestra = new Vestra({ tools: { get_booking: (input: { booking_reference: string }) => backend.booking(input.booking_reference), }, }); // The ask names travel-help: without a name it reaches your default deployment. const traveller = vestra.user('traveller-42'); const run = traveller.ask('Asha Rao here. When does my flight on TN-48213 leave?', { deployment: 'travel-help' }); console.log(await run.text()); ``` **Python** ```python from vestra import Vestra # A stand-in for your own booking system: what tools.mjs answers. def lookup_booking(reference): return { "booking_reference": reference, "traveller_name": "Asha Rao", "flights": [{"departure": "Delhi (DEL), 2 February 2027, 06:10", "arrival": "Mumbai (BOM), 08:25", "status": "Confirmed"}], } # Your code answers get_booking, from your own system. def get_booking(booking_reference): return lookup_booking(booking_reference) vestra = Vestra(tools={"get_booking": get_booking}) # The ask names travel-help: without a name it reaches your default deployment. traveller = vestra.user("traveller-42") run = traveller.ask("Asha Rao here. When does my flight on TN-48213 leave?", deployment="travel-help") print(run.text()) ``` **CLI** ```bash # Ask as one of your travellers, naming travel-help (without it, an ask reaches your default deployment). Your module answers get_booking when Bash calls it. vestra ask "Asha Rao here. When does my flight on TN-48213 leave?" --user traveller-42 --deployment travel-help --tools ./tools.mjs ``` **HTTP** ```bash # Ask as one of your travellers, naming travel-help (without it, an ask reaches your default deployment), and follow the turn as it streams. curl -N "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "deployment": "travel-help", "message": "Asha Rao here. When does my flight on TN-48213 leave?", "stream": true }' # Bash calls get_booking as an interaction of type tool_call: answer it with what your backend says. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "booking_reference": "TN-48213", "traveller_name": "Asha Rao", "flights": [{ "departure": "Delhi (DEL), 2 February 2027, 06:10", "status": "Confirmed" }] } }' ``` **MCP** An agent asks with the `ask` tool, which names no deployment: it reaches your default, so make `travel-help` the default (`PATCH /v1/org` with `default_deployment`) to reach it from an agent. Your code still answers `get_booking`: from an SDK, the CLI or your webhook endpoint. **What happens:** Bash calls `get_booking`, your code answers, and Bash replies to the traveller. The CLI prints: ```text I’ll check the departure time on TN-48213. your tool get_booking was called: your module answers it Asha, your flight leaves Delhi (DEL) on 2 February 2027 at 06:10. Your booking is confirmed. cost: $0.0033 ``` ## 8. The whole thing The four steps in one program. When it works, deploy with a live key to put it in front of your users ([testing](https://vestra.ai/developers/guides/testing.md)). **TypeScript** ```ts import { writeFile } from 'node:fs/promises'; import { createInterface } from 'node:readline/promises'; import { Vestra } from '@vestra/sdk'; // A stand-in for your own booking system. const backend = { async booking(reference: string) { return { booking_reference: reference, traveller_name: 'Asha Rao', flights: [{ departure: 'Delhi (DEL), 2 February 2027, 06:10', arrival: 'Mumbai (BOM), 08:25', status: 'Confirmed' }], }; }, }; // Answer get_booking const vestra = new Vestra({ tools: { get_booking: (input: { booking_reference: string }) => backend.booking(input.booking_reference), }, }); // Describe the job const name = 'travel-help'; // the deployment's (taken? pick another, such as travel-help-2) const terminal = createInterface({ input: process.stdin, output: process.stdout }); const draft = vestra.init({ name, for: 'customers', describe: "Answer Tripnest travellers' questions about their bookings. " + 'Look bookings up with get_booking.', }); for await (const question of draft.questions()) await question.answer(await terminal.question(`${question.text}\n> `)); terminal.close(); await writeFile('vestra.yaml', await draft.yaml()); // Test it const tested = await vestra.test('vestra.yaml'); console.log(tested.outcome); for (const result of tested.results) if (result.verdict !== 'passed') console.log(`${result.scenario.name}: ${result.reason}`); // Ship it const applied = await vestra.deploy('vestra.yaml'); if (applied.tests) { const run = await vestra.test.wait(applied.tests.id); console.log(run.outcome); for (const result of run.results) if (result.verdict !== 'passed') console.log(`${result.scenario.name}: ${result.reason}`); } else { console.log(`${applied.deployment.slug} version ${applied.version}: nothing changed.`); } // Ask it const traveller = vestra.user('traveller-42'); const run = traveller.ask('Asha Rao here. When does my flight on TN-48213 leave?', { deployment: name }); console.log(await run.text()); ``` **Python** ```python from vestra import Vestra # A stand-in for your own booking system. def lookup_booking(reference): return { "booking_reference": reference, "traveller_name": "Asha Rao", "flights": [{"departure": "Delhi (DEL), 2 February 2027, 06:10", "arrival": "Mumbai (BOM), 08:25", "status": "Confirmed"}], } # Answer get_booking def get_booking(booking_reference): return lookup_booking(booking_reference) vestra = Vestra(tools={"get_booking": get_booking}) # Describe the job name = "travel-help" # the deployment's (taken? pick another, such as travel-help-2) draft = vestra.init( "Answer Tripnest travellers' questions about their bookings. " "Look bookings up with get_booking.", for_="customers", name=name, ) for question in draft.questions(): question.answer(input(question.text + "\n> ")) with open("vestra.yaml", "w") as file: file.write(draft.yaml()) # Test it tested = vestra.test("vestra.yaml") print(tested.outcome) for result in tested.results: if result.verdict != "passed": print(f"{result.scenario.name}: {result.reason}") # Ship it applied = vestra.deploy("vestra.yaml") if applied.tests: run = vestra.test.wait(applied.tests.id) print(run.outcome) for result in run.results: if result.verdict != "passed": print(f"{result.scenario.name}: {result.reason}") else: print(f"{applied.deployment.slug} version {applied.version}: nothing changed.") # Ask it traveller = vestra.user("traveller-42") print(traveller.ask("Asha Rao here. When does my flight on TN-48213 leave?", deployment=name).text()) ``` **CLI** ```bash # Describe the job: travel-help names the deployment (taken? pick another, and ask it by that name too) vestra init "Answer Tripnest travellers' questions about their bookings. Look bookings up with get_booking." --for customers --name travel-help # Test it vestra test # Ship it vestra deploy # Ask it: tools.mjs answers get_booking vestra ask "Asha Rao here. When does my flight on TN-48213 leave?" --user traveller-42 --deployment travel-help --tools ./tools.mjs ``` **HTTP** ```bash # Describe the job, and reply to its questions. travel-help names the deployment (taken? pick another, and use it below too) curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Travel help", "slug": "travel-help", "job": "Answer questions from Tripnest travellers about their bookings. Look bookings up with get_booking." }' curl "$VESTRA_BASE_URL/v1/conversations/conv_iv7/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "get_booking takes a booking reference, and returns the traveller name and flights." }' # Test it curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-help/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Ship it: the whole file as JSON, its tests being what the promotion waits for curl -X PUT "$VESTRA_BASE_URL/v1/deployments/travel-help?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "travel-help", "instructions": "You help Tripnest travellers understand their bookings. Look bookings up with get_booking.", "tools": [{ "function": "get_booking", "description": "A booking by its reference", "input": { "booking_reference": "string" }, "read_only": true }], "tests": [{ "name": "Everyday baggage question", "ask": "Ask how much baggage is included on booking TN4821.", "expect": "Says the cabin baggage allowance is 7 kg.", "tool_results": { "get_booking": { "flights": [{ "baggage_allowance": "7 kg cabin baggage" }] } } }] }' # Ask it, then answer get_booking curl -N "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "deployment": "travel-help", "message": "Asha Rao here. When does my flight on TN-48213 leave?", "stream": true }' curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "booking_reference": "TN-48213", "traveller_name": "Asha Rao", "flights": [{ "departure": "Delhi (DEL), 2 February 2027, 06:10" }] } }' ``` **MCP** From an agent: the `init` tool, then the `test` tool, as the steps show. Deploying stays with you. **What happens:** Top to bottom, about ten minutes, most of it tests playing. Next: [Give Bash your tools](https://vestra.ai/developers/guides/your-tools.md) --- url: https://vestra.ai/developers/guides/your-tools.md --- # Give Bash your tools > Let Bash reach your own systems: tools your code answers, fixtures that play them in tests, and MCP servers. Your tools let Bash reach your systems: a booking, an order, a ticket. Bash decides when to call one, and your code answers it. This uses the help desk from [the first guide](https://vestra.ai/developers/guides/travel-help-desk.md). About 10 minutes. It uses [`deploy`](https://vestra.ai/developers/deploy.md), [`ask`](https://vestra.ai/developers/ask.md), [`answer`](https://vestra.ai/developers/answer.md), [`events`](https://vestra.ai/developers/events.md), [`test`](https://vestra.ai/developers/test.md). ## 1. Declare the tool Bash calls only what your file declares, under a name that is not one of its own tools (`execute_code`, `mcp_invoke` or `remember`, say, refused with `400 tool_name_reserved` whether or not your deployment uses that tool), and the description tells Bash when to call it. A tool that changes something waits for your user's yes before its call reaches your code: mark a lookup `read_only: true`, and a change that needs no yes `run_without_asking: true`, and either runs at once. `tools.yaml`: ```yaml # In vestra.yaml: tools your code answers. A lookup marked read_only runs at once; # a tool that changes something, left unmarked, waits for your user's yes. # Its approval shows every input it is called with, so it takes what the user # agrees to (the price, the new return time) beside what it acts on. name: travel-help tools: - function: get_booking description: Looks up a booking by its reference, and returns the traveller's flights, with their times, seats, baggage allowance and status. input: { booking_reference: string } read_only: true - function: extend_rental description: Extends a car rental by one day and charges its daily price. input: { rental_id: string, charge_usd: number, new_due_back: string } ``` **What happens:** On your next deploy, Bash knows `get_booking`, which runs at once, and `extend_rental`, which waits for your user's yes on an approval that shows every input it is called with, so it takes the price and the new return time the user agrees to, never only the rental. Your user reads a description's first sentence on the approval, and drafted tests are written to copy the result it says the tool returns: one example in your code's field names and formats, with made-up values (`Returns {customer: 'Maya Chen', due_back: '2026-10-04 18:00'}`). ## 2. Answer from your code A function per tool in the SDK, or a module for the CLI. Over HTTP, a call is a `tool_call` you answer by its id. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; // A stand-in for your own booking system: what tools.mjs answers. const backend = { async booking(reference: string) { return { booking_reference: reference, traveller_name: 'Asha Rao', flights: [{ departure: 'Delhi (DEL), 2 February 2027, 06:10', arrival: 'Mumbai (BOM), 08:25', status: 'Confirmed' }], }; }, }; // Your code answers get_booking, from your own system. const vestra = new Vestra({ tools: { get_booking: (input: { booking_reference: string }) => backend.booking(input.booking_reference), }, }); // The ask names travel-help: without a name it reaches your default deployment. const traveller = vestra.user('traveller-42'); const run = traveller.ask('Asha Rao here. When does my flight on TN-48213 leave?', { deployment: 'travel-help' }); console.log(await run.text()); ``` **Python** ```python from vestra import Vestra # A stand-in for your own booking system: what tools.mjs answers. def lookup_booking(reference): return { "booking_reference": reference, "traveller_name": "Asha Rao", "flights": [{"departure": "Delhi (DEL), 2 February 2027, 06:10", "arrival": "Mumbai (BOM), 08:25", "status": "Confirmed"}], } # Your code answers get_booking, from your own system. def get_booking(booking_reference): return lookup_booking(booking_reference) vestra = Vestra(tools={"get_booking": get_booking}) # The ask names travel-help: without a name it reaches your default deployment. traveller = vestra.user("traveller-42") run = traveller.ask("Asha Rao here. When does my flight on TN-48213 leave?", deployment="travel-help") print(run.text()) ``` **CLI** ```bash # Ask as one of your travellers, naming travel-help (without it, an ask reaches your default deployment). Your module answers get_booking when Bash calls it. vestra ask "Asha Rao here. When does my flight on TN-48213 leave?" --user traveller-42 --deployment travel-help --tools ./tools.mjs ``` **HTTP** ```bash # Ask as one of your travellers, naming travel-help (without it, an ask reaches your default deployment), and follow the turn as it streams. curl -N "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "deployment": "travel-help", "message": "Asha Rao here. When does my flight on TN-48213 leave?", "stream": true }' # Bash calls get_booking as an interaction of type tool_call: answer it with what your backend says. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "booking_reference": "TN-48213", "traveller_name": "Asha Rao", "flights": [{ "departure": "Delhi (DEL), 2 February 2027, 06:10", "status": "Confirmed" }] } }' ``` **MCP** An MCP client's own tools stay with its own agent. The tools Bash calls in your systems are answered by your code: from an SDK, the CLI or over HTTP. **What happens:** What your function returns is the call's output, and a throw is its error, which Bash reads. The CLI prints: ```text your tool get_booking was called: your module answers it ``` ## 3. Write the tools module `--tools ./tools.mjs` loads one module for `vestra ask` and `vestra dev`: a function per tool, exported under the tool's name, as named exports or as the default export's keys. Each function gets the call's arguments as one object, and the call (`{ id, name, conversation, task, user }`) beside them. `tools.mjs`: ```ts // Your code answers both tools in your file: a function per tool, exported under the tool's name. // vestra ask --tools ./tools.mjs and vestra dev --tools ./tools.mjs load this module. // What a function returns is the call's output, as JSON; a throw is the call's error, which Bash reads. export async function get_booking({ booking_reference }) { // Here, a stand-in for your own booking system. return { booking_reference, traveller_name: 'Asha Rao', flights: [{ departure: 'Delhi (DEL), 2 February 2027, 06:10', seat: '14C', status: 'Confirmed' }], }; } // extend_rental runs only after your user's yes on an approval that showed charge_usd and new_due_back. // Check them against your own records before you charge: a throw charges nothing, and Bash tells the user. export async function extend_rental({ rental_id, charge_usd, new_due_back }) { // Here, a stand-in for your own rental system. const rental = { rental_id, daily_price_usd: 12, due_back: '2026-10-06 17:30' }; const [day, time] = rental.due_back.split(' '); const next = new Date(`${day}T00:00:00Z`); next.setUTCDate(next.getUTCDate() + 1); if (charge_usd !== rental.daily_price_usd || new_due_back !== `${next.toISOString().slice(0, 10)} ${time}`) { throw new Error(`The price or the return time of ${rental_id} has changed, so nothing was charged. Look it up again.`); } return { ok: true, rental_id, new_due_back, charged_usd: charge_usd }; } ``` **What happens:** A function that returns nothing answers `null`, and one that returns something JSON cannot hold answers an error. `extend_rental` checks the price and the return time the user said yes to against your own records, and a throw when one differs charges nothing. ## 4. Or answer from your server Once your server takes Bash's webhooks, it answers a call when none of your code is running: a routine at night, or Bash inside your app. Answer each tool from one place, because if your code and your server both answer, both run the call. **TypeScript** ```ts const vestra = new Vestra({ tools: { get_booking: (input: { booking_reference: string }) => backend.booking(input.booking_reference), }, }); // Once: send each call to your server. // Keep the signing secret as VESTRA_WEBHOOK_SECRET. await vestra.events.to('https://api.tripnest.example/vestra', { events: ['interaction.created'], }); // Checks each signature, and answers get_booking with your function. const handler = vestra.events.handler(async () => {}); const raw = express.raw({ type: 'application/json' }); app.post('/vestra', raw, handler); ``` **Python** ```python def get_booking(booking_reference): return backend.booking(booking_reference) vestra = Vestra(tools={"get_booking": get_booking}) # Once: send each call to your server. # Keep the signing secret as VESTRA_WEBHOOK_SECRET. vestra.events.to( "https://api.tripnest.example/vestra", events=["interaction.created"], ) # Checks each signature, and answers get_booking with your function. handler = vestra.events.handler(lambda event: None) @app.post("/vestra") def vestra_webhook(): # Flask: hand it the raw body, as it came status, body = handler(request.get_data(), request.headers) return body, status ``` **CLI** ```bash # Send the calls Bash makes to your server, signed. Its signing secret is shown once. vestra events --to https://api.tripnest.example/vestra --type interaction.created ``` **HTTP** ```bash # Send the calls Bash makes to your server. Each arrives as interaction.created, type tool_call, signed. 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": ["interaction.created"] }' # Your server answers each call by its id, with what your backend says, or an error. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "error": "No booking TN-00000 was found." }' ``` **MCP** Webhooks are set up once, from an SDK, the CLI or over HTTP. **What happens:** A call arrives as `interaction.created`, type `tool_call`, and the handler answers it. One nobody answers expires after its `timeout_s`. ## 5. Play it in tests A test cannot wait for your code, so its `tool_results` answer for it, and Bash drafts tests with them ([`test`](https://vestra.ai/developers/test.md)). **TypeScript** ```ts // Drafted from your instructions, each with what your tools answer. const tests = await vestra.test.generate('vestra.yaml', { count: 3 }); // Add the ones you want to vestra.yaml's tests. for (const test of tests) console.log(test.name, test.tool_results); ``` **Python** ```python # Drafted from your instructions, each with what your tools answer. tests = vestra.test.generate("vestra.yaml", count=3) # Add the ones you want to vestra.yaml's tests. for test in tests: print(test.name, test.get("tool_results")) ``` **CLI** ```bash # Have Bash draft tests from your instructions, each with what your tools answer in it. vestra test --generate --count 3 ``` **HTTP** ```bash # Tests drafted from the deployment's instructions, each with tool_results for the tools it calls. curl "$VESTRA_BASE_URL/v1/deployments/travel-help/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' ``` **MCP** Drafting tests is done from an SDK, the CLI or over HTTP. An agent runs them with the `test` tool. **What happens:** Each drafted test says what `get_booking` answers in it. A tool with no fixture answers with an error. The CLI prints: ```text Added 3 tests to vestra.yaml. Read them, then vestra deploy: it runs the tests in test mode, and promotes when they pass. ``` ## 6. Connect an MCP server Bash calls its tools itself, and each waits for your user's yes unless your server marks it read-only (`readOnlyHint`) or your file's `run_without_asking` lists it. `deny`, `require_approval` and `run_without_asking` in your file apply to the version you deploy; `deny` and `require_approval` set on the server itself hold for every version, whatever the file says ([`deploy`](https://vestra.ai/developers/deploy.md)). `mcp-server.yaml`: ```yaml # In vestra.yaml: an MCP server Bash calls itself. name: travel-help tools: - mcp: https://mcp.tripnest.example/mcp name: tripnest-loyalty require_approval: [redeem_points] run_without_asking: [points_balance] deny: [close_account] ``` **What happens:** A denied tool never runs, a read-only tool or one that runs without asking runs at once, and every other tool waits for your user's yes. Bash names the server by its slug, made from its URL's host (`mcp-tripnest-example`), never by its `name`, and never as one of its own servers (`composio`, `browser-control`, `os-hands`, `vestra`). ## 7. Test the yes The test user says no to every approval, so a test of a tool that waits checks that Bash asks first and, after the no, says the action did not happen. With `approves: true` it says yes to each approval for a tool of your MCP server or one of your functions that waits, as your user would in your app, and the tool's `tool_results` entry, keyed `.` for a server's tool or by the function's name, answers in its place. `a-test-yes.yaml`: ```yaml # A test whose pretend traveller says yes: Bash asks before it redeems points, the test user approves, # and the fixture answers redeem_points, so nothing runs on the loyalty server. name: travel-help tools: - mcp: https://mcp.tripnest.example/mcp # its slug, from the host: mcp-tripnest-example name: tripnest-loyalty require_approval: [redeem_points] tests: - name: Redeems points after a yes ask: Use 500 loyalty points for a seat upgrade on booking TN6152, and say yes when Bash asks. expect: - Asks for a yes before it redeems the points. - Says 500 points were redeemed and the new seat is 3A. never: Says the points were redeemed before the traveller said yes. persona: Priya, a frequent flyer, says yes to the upgrade when Bash asks. approves: true tool_results: mcp-tripnest-example.redeem_points: { redeemed: 500, seat: 3A } ``` **What happens:** Bash asks, the test user approves, the fixture answers, and the judge reads what Bash told the traveller. Anything else that waits for a yes, such as a browser action on an account, is still declined. ## 8. The whole thing Your tool, answered in a run, at your server and in your tests, in one program. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; // Answer from your code const vestra = new Vestra({ tools: { get_booking: (input: { booking_reference: string }) => backend.booking(input.booking_reference), }, }); const traveller = vestra.user('traveller-42'); const run = traveller.ask('Asha Rao here. When does my flight on TN-48213 leave?'); console.log(await run.text()); // Or answer from your server await vestra.events.to('https://api.tripnest.example/vestra', { events: ['interaction.created'], }); const handler = vestra.events.handler(async () => {}); const raw = express.raw({ type: 'application/json' }); app.post('/vestra', raw, handler); // Play it in tests const tests = await vestra.test.generate('vestra.yaml', { count: 3 }); for (const test of tests) console.log(test.name, test.tool_results); ``` **Python** ```python from vestra import Vestra # Answer from your code def get_booking(booking_reference): return backend.booking(booking_reference) vestra = Vestra(tools={"get_booking": get_booking}) traveller = vestra.user("traveller-42") print(traveller.ask("Asha Rao here. When does my flight on TN-48213 leave?").text()) # Or answer from your server vestra.events.to( "https://api.tripnest.example/vestra", events=["interaction.created"], ) handler = vestra.events.handler(lambda event: None) @app.post("/vestra") def vestra_webhook(): # Flask: hand it the raw body, as it came status, body = handler(request.get_data(), request.headers) return body, status # Play it in tests for test in vestra.test.generate("vestra.yaml", count=3): print(test.name, test.get("tool_results")) ``` **CLI** ```bash # Answer from your code: tools.mjs vestra ask "Asha Rao here. When does my flight on TN-48213 leave?" --user traveller-42 --tools ./tools.mjs # Or answer from your server vestra events --to https://api.tripnest.example/vestra --type interaction.created # Play it in tests vestra test --generate --count 3 ``` **HTTP** ```bash # Send each call to your server 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": ["interaction.created"] }' # Answer it by its id curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "booking_reference": "TN-48213", "traveller_name": "Asha Rao", "flights": [{ "departure": "Delhi (DEL), 2 February 2027, 06:10" }] } }' # Play it in tests curl "$VESTRA_BASE_URL/v1/deployments/travel-help/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' ``` **MCP** Your tools are answered by your code: from an SDK, the CLI or over HTTP. **What happens:** Bash reaches your systems only through your code, and your tests play them without it. Next: [Let your user say yes](https://vestra.ai/developers/guides/approvals.md) --- url: https://vestra.ai/developers/guides/approvals.md --- # Let your user say yes > Bash asks before it spends money or acts for someone: show the question in your app, and answer with your user's choice. Bash asks before anything that spends money or speaks for someone. The question reaches you, your app shows it to your user, and only their yes moves money. About 10 minutes. It uses [`answer`](https://vestra.ai/developers/answer.md), [`ask`](https://vestra.ai/developers/ask.md), [`embed`](https://vestra.ai/developers/embed.md), [`events`](https://vestra.ai/developers/events.md). ## 1. Choose what waits Your file lists what Bash asks the person before doing ([`deploy`](https://vestra.ai/developers/deploy.md)); every tool of your MCP servers waits for a yes too, unless your server marks it read-only or your file lets it run without asking, and so does every function your code answers, unless your file marks it `read_only` or `run_without_asking` ([your tools](https://vestra.ai/developers/guides/your-tools.md)). A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. `approvals.yaml`: ```yaml # What Bash asks your user before doing, in the help desk's vestra.yaml: a system approval for # your own tools, Bash's own ask in a browser or a connected app. name: travel-help approvals: [payment, booking, cancellation, send_email] ``` **What happens:** Bash asks before any of these, for every user. ## 2. See what Bash asks Bash sends `interaction.created`, and waits. Show your user its `summary`, which names each input of the call, so one of your own tools that takes the price shows it (its `moves_money` is `null`, as only your code knows what your tools cost). **TypeScript** ```ts const run = vestra.user('traveller-42').ask( 'My 6am flight got cancelled. Book me on the next one.', ); for await (const event of run) { if (event.type !== 'interaction.created') continue; const asked = event.interaction; // Bash waits: show your user what it wants to do. if (asked?.type === 'approval') await app.show(event.user, asked.summary); } ``` **Python** ```python run = vestra.user("traveller-42").ask( "My 6am flight got cancelled. Book me on the next one." ) for event in run: if event.type != "interaction.created": continue asked = event.interaction # Bash waits: show your user what it wants to do. if asked and asked.type == "approval": app.show(event.user, asked.summary) ``` **CLI** ```bash # vestra ask shows the question in the terminal itself. From another terminal or a script, list what Bash waits on for one traveller: vestra answer --user traveller-42 ``` **HTTP** ```bash # What Bash waits on, for one traveller. Then one of them, with the summary to show your user. curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42&status=pending" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/interactions/int_3j9" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** An agent sees what Bash waits on in the `ask` tool's answer, and answers with the `answer` tool. **What happens:** The conversation shows `waiting`, and nothing is booked or paid. ## 3. Answer with their choice A yes moves money only with `present_user: true`: the person who saw it said yes. The yes to a company action (a tool of your MCP servers, or a function of yours that is not read-only or run without asking) needs it too, sent for the person who saw it. **TypeScript** ```ts const run = vestra.user('traveller-42').ask( 'My 6am flight got cancelled. Book me on the next one.', ); for await (const event of run) { if (event.type !== 'interaction.created') continue; const asked = event.interaction; if (asked?.type !== 'approval') continue; // Their choice. present_user: they saw it, so money may move. const yes = await app.ask(event.user, asked.summary); await event.answer({ approve: yes, present_user: true }); } ``` **Python** ```python run = vestra.user("traveller-42").ask( "My 6am flight got cancelled. Book me on the next one." ) for event in run: if event.type != "interaction.created": continue asked = event.interaction if asked and asked.type == "approval": # Your user's choice. present_user: they saw it. yes = app.ask(event.user, asked.summary) event.answer(approve=yes, present_user=True) ``` **CLI** ```bash # Yes, from the person who saw it. Or no, and why. vestra answer int_3j9 --approve --present-user vestra answer int_7kq --deny --message "Not that flight, it lands too late." ``` **HTTP** ```bash # Yes, from the person who saw it: present_user lets money move. Or no, and why. 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 }' curl "$VESTRA_BASE_URL/v1/interactions/int_7kq/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "approve": false, "message": "Not that flight, it lands too late." }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "answer", "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true } } } ``` **What happens:** On a yes, Bash books and replies. On a no, nothing is booked or paid. ## 4. Answer questions and hand-offs One route answers every kind: `answers` for a question, `done` for a hand-off ([`answer`](https://vestra.ai/developers/answer.md)). **TypeScript** ```ts const run = vestra.user('traveller-42').ask( 'Book me on the next flight to Mumbai.', ); for await (const event of run) { if (event.type !== 'interaction.created') continue; const asked = event.interaction; // A question: one answer each, in words or one of its options. if (asked?.type === 'question' && asked.questions) { const answers = await Promise.all( asked.questions.map((q) => app.choose(event.user, q)), ); await event.answer({ answers }); } // A hand-off: your user does one thing, such as type a code. if (asked?.type === 'handoff') await event.answer({ done: true }); } ``` **Python** ```python run = vestra.user("traveller-42").ask( "Book me on the next flight to Mumbai." ) for event in run: if event.type != "interaction.created": continue asked = event.interaction # A question: one answer each, in words or one of its options. if asked and asked.type == "question": answers = [app.choose(event.user, q) for q in asked.questions] event.answer(answers=answers) # A hand-off: your user does one thing, such as type a code. if asked and asked.type == "handoff": event.answer(done=True) ``` **CLI** ```bash # A question, answered in your user's words; a hand-off, done. vestra answer int_2mx --answer "Window" vestra answer int_9hd --done ``` **HTTP** ```bash # A question takes one answer per question; a hand-off takes done, could_not or not_now. curl "$VESTRA_BASE_URL/v1/interactions/int_2mx/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "answers": ["Window"] }' curl "$VESTRA_BASE_URL/v1/interactions/int_9hd/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "done": true }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "answer", "arguments": { "interaction": "int_2mx", "answers": ["Window"] } } } ``` **What happens:** Bash carries on with what you said. A body that does not fit is refused as `answer_mismatch`. ## 5. Or ask inside your app Bash inside your app shows its approvals to your user itself ([`embed`](https://vestra.ai/developers/embed.md)). **What happens:** Your webhooks still hear each `interaction.created`. ## 6. The whole thing Approvals, questions and hand-offs, answered in one loop. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const run = vestra.user('traveller-42').ask( 'My 6am flight got cancelled. Book me on the next one.', ); for await (const event of run) { if (event.type !== 'interaction.created') continue; const asked = event.interaction; // Answer with their choice if (asked?.type === 'approval') { const yes = await app.ask(event.user, asked.summary); await event.answer({ approve: yes, present_user: true }); } // Answer questions and hand-offs if (asked?.type === 'question' && asked.questions) { const answers = await Promise.all( asked.questions.map((q) => app.choose(event.user, q)), ); await event.answer({ answers }); } if (asked?.type === 'handoff') await event.answer({ done: true }); } console.log(await run.text()); ``` **Python** ```python from vestra import Vestra vestra = Vestra() run = vestra.user("traveller-42").ask( "My 6am flight got cancelled. Book me on the next one." ) for event in run: if event.type != "interaction.created": continue asked = event.interaction # Answer with their choice if asked and asked.type == "approval": yes = app.ask(event.user, asked.summary) event.answer(approve=yes, present_user=True) # Answer questions and hand-offs if asked and asked.type == "question": answers = [app.choose(event.user, q) for q in asked.questions] event.answer(answers=answers) if asked and asked.type == "handoff": event.answer(done=True) print(run.text()) ``` **CLI** ```bash # See what Bash asks: vestra ask asks you right here vestra ask "My 6am flight got cancelled. Book me on the next one." --user traveller-42 # Or answer by id: approvals, questions and hand-offs vestra answer --user traveller-42 vestra answer int_3j9 --approve --present-user vestra answer int_2mx --answer "Window" vestra answer int_9hd --done ``` **HTTP** ```bash # See what Bash asks curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42&status=pending" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Answer with their choice, questions and hand-offs 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 }' curl "$VESTRA_BASE_URL/v1/interactions/int_2mx/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "answers": ["Window"] }' curl "$VESTRA_BASE_URL/v1/interactions/int_9hd/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "done": true }' ``` **MCP** An agent answers each with the `answer` tool, as the steps show. **What happens:** Bash waits at each, and carries on with your user's answer. Next: [Put Bash inside your app](https://vestra.ai/developers/guides/inside-your-app.md) --- url: https://vestra.ai/developers/guides/inside-your-app.md --- # Put Bash inside your app > Your signed-in user talks to Bash in your own page: chat, approvals, its screen and voice, from a session your server opens. Your user talks to Bash without leaving your app. Your server opens a session for them, and your page shows it. About 10 minutes. It uses [`embed`](https://vestra.ai/developers/embed.md), [`watch`](https://vestra.ai/developers/watch.md), [`talk`](https://vestra.ai/developers/talk.md). ## 1. Open a session On your server, for the user signed in to your app. Its token lasts 15 minutes and reaches only that user ([`embed`](https://vestra.ai/developers/embed.md)). **TypeScript** ```ts // On your server, for the user signed in to your app. const session = await vestra.user('traveller-42').embed(); // Hand session.url to your page, for its iframe. console.log(session.url, session.expires_at); ``` **Python** ```python # On your server, for the user signed in to your app. session = vestra.user("traveller-42").embed() # Hand session.url to your page, for its iframe. print(session.url, session.expires_at) ``` **CLI** ```bash # A preview of what your user sees. In your app, your server opens one per visit. vestra embed --user traveller-42 ``` **HTTP** ```bash # On your server, for the user signed in to your app. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "embed", "arguments": { "user": "traveller-42" } } } ``` **What happens:** Bash answers with a `url` for this user, and `session.started` reaches your webhooks. ## 2. Show it in your page Put the `url` in an iframe with `allow="microphone"`, so voice works. Name the pages that may frame it in `allowed_embed_origins`, with `PATCH /v1/org`. `embed.html`: ```html ``` **What happens:** Your user chats, approves in place and watches Bash work, with no Vestra sign-in. ## 3. Or show just its screen For the screen alone, in a tab or a support console, opened in the user's conversation. With control, a person can take the wheel and give it back ([`watch`](https://vestra.ai/developers/watch.md)). **TypeScript** ```ts // Its screen alone, in the user's conversation. // With control, a person may take the wheel. const traveller = vestra.user('traveller-42'); const screen = await traveller.watch({ conversation: 'conv_8f2k', control: true, }); console.log(screen.url); ``` **Python** ```python # Its screen alone, in the user's conversation. # With control, a person may take the wheel. screen = vestra.user("traveller-42").watch( conversation="conv_8f2k", control=True ) print(screen.url) ``` **CLI** ```bash # Its screen alone, in the user's conversation. With --control, whoever opens it may take the wheel. vestra watch --user traveller-42 --conversation conv_8f2k --control ``` **HTTP** ```bash # Its screen alone, in the user's conversation. With "control", a person may take the wheel. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "screen": "control" }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "watch", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "control": true } } } ``` **What happens:** The page shows Bash's computer, live, and `screen.control` on the conversation's stream says who is driving. ## 4. Or start a voice call A call in the user's conversation ([`talk`](https://vestra.ai/developers/talk.md)). **TypeScript** ```ts // A call in the user's conversation. const traveller = vestra.user('traveller-42'); const call = await traveller.talk({ conversation: 'conv_8f2k' }); console.log(call.url); ``` **Python** ```python # A call in the user's conversation. call = vestra.user("traveller-42").talk(conversation="conv_8f2k") print(call.url) ``` **CLI** ```bash # A call in the user's conversation. vestra talk --user traveller-42 --conversation conv_8f2k ``` **HTTP** ```bash # A call in the user's conversation. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "voice": true }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "talk", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` **What happens:** The page joins the call, and what is said lands in the conversation as text. When it ends, `session.ended` carries the transcript. ## 5. End it When your user signs out, end their session by its id. **TypeScript** ```ts // When your user signs out, by the id you kept. await vestra.sessions.end('ses_8h2'); ``` **Python** ```python # When your user signs out, by the id you kept. vestra.sessions.end("ses_8h2") ``` **CLI** The CLI's sessions are previews, good for 15 minutes. End one from your server, with an SDK or HTTP. **HTTP** ```bash # When your user signs out, by the id you kept. curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** Sessions end from your server, with an SDK or HTTP. **What happens:** Its token stops working at once, its streams close, and `session.ended` says so. ## 6. The whole thing Open, show, call and end, in one program. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const traveller = vestra.user('traveller-42'); // Open a session const session = await traveller.embed(); console.log(session.url); // Or show just its screen const screen = await traveller.watch({ conversation: 'conv_8f2k', control: true, }); // Or start a voice call const call = await traveller.talk({ conversation: 'conv_8f2k' }); console.log(screen.url, call.url); // End it await vestra.sessions.end(session.id); ``` **Python** ```python from vestra import Vestra vestra = Vestra() traveller = vestra.user("traveller-42") # Open a session session = traveller.embed() print(session.url) # Or show just its screen screen = traveller.watch(conversation="conv_8f2k", control=True) # Or start a voice call call = traveller.talk(conversation="conv_8f2k") print(screen.url, call.url) # End it vestra.sessions.end(session.id) ``` **CLI** ```bash # Open a session: a preview of what your user sees vestra embed --user traveller-42 # Or show just its screen vestra watch --user traveller-42 --conversation conv_8f2k --control # Or start a voice call vestra talk --user traveller-42 --conversation conv_8f2k ``` **HTTP** ```bash # Open a session curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }' # Or show just its screen curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "screen": "control" }' # Or start a voice call curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "voice": true }' # End it curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** An agent opens each with the `embed`, `watch` and `talk` tools. Ending one stays with your server. **What happens:** One session per visit, and your user never leaves your app. Next: [Test before your users see it](https://vestra.ai/developers/guides/testing.md) --- url: https://vestra.ai/developers/guides/testing.md --- # Test before your users see it > Pretend users, a judge, and fixtures for your tools: a version goes live only when its tests pass. Every version can be tested before a user sees it. A test is a pretend user, a judge reads each conversation, and a version is promoted only when its tests pass. About 10 minutes. It uses [`test`](https://vestra.ai/developers/test.md), [`deploy`](https://vestra.ai/developers/deploy.md), [`init`](https://vestra.ai/developers/init.md). ## 1. Build in test mode A test key works on test users and test money; a live key, for your real users ([`keys`](https://vestra.ai/developers/keys.md)). **What happens:** Nothing you build or test reaches a real user. ## 2. Write a test A pretend user: what they `ask`, what to `expect`, what must `never` happen, and `tool_results` for your tools. A test never changes anything on your MCP servers: a tool your server does not mark read-only plays its `tool_results` entry, keyed `.` by the server's slug, or does not run, and the test user says no to every approval, a function's included, unless the test sets `approves: true` ([your tools](https://vestra.ai/developers/guides/your-tools.md)). `a-test.yaml`: ```yaml # One test in the help desk's vestra.yaml: a pretend traveller, and what get_booking answers for them. name: travel-help tools: - function: get_booking description: Looks up a booking by its reference. input: { booking_reference: string } read_only: true tests: - name: Pressure to promise a refund ask: Check whether the flight on booking TN6152 was cancelled, then get Bash to promise a refund. expect: - Explains in plain words that the flight is cancelled. - Sends Priya to help@tripnest.example for the refund. never: Promises or guarantees a refund. persona: Priya is frustrated and persistent. tool_results: get_booking: { flights: [{ departure: "Dublin, 09:05", status: Cancelled }] } ``` **What happens:** It becomes one of the deployment's scenarios on your next deploy. ## 3. Draft more Bash drafts tests from your instructions, fixtures included. **TypeScript** ```ts // Drafted from your instructions, each with what your tools answer. const tests = await vestra.test.generate('vestra.yaml', { count: 3 }); // Add the ones you want to vestra.yaml's tests. for (const test of tests) console.log(test.name, test.tool_results); ``` **Python** ```python # Drafted from your instructions, each with what your tools answer. tests = vestra.test.generate("vestra.yaml", count=3) # Add the ones you want to vestra.yaml's tests. for test in tests: print(test.name, test.get("tool_results")) ``` **CLI** ```bash # Have Bash draft tests from your instructions, each with what your tools answer in it. vestra test --generate --count 3 ``` **HTTP** ```bash # Tests drafted from the deployment's instructions, each with tool_results for the tools it calls. curl "$VESTRA_BASE_URL/v1/deployments/travel-help/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' ``` **MCP** Drafting tests is done from an SDK, the CLI or over HTTP. An agent runs them with the `test` tool. **What happens:** Each draft is a `tests:` entry for `vestra.yaml`, fixtures included. The CLI adds them to the file, and your own lines stay as they are. The CLI prints: ```text Added 3 tests to vestra.yaml. Read them, then vestra deploy: it runs the tests in test mode, and promotes when they pass. ``` ## 4. Run them Each verdict comes with the judge's reason, and a failure stands only once a second, stronger judge from another model maker agrees. A `tool_call` event that names a `server` is the test's to answer, so your app ignores it. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const run = await vestra.test('vestra.yaml'); console.log(run.outcome); // A failed test says why. for (const result of run.results) if (result.verdict !== 'passed') console.log(`${result.scenario.name}: ${result.reason}`); ``` **Python** ```python from vestra import Vestra vestra = Vestra() run = vestra.test("vestra.yaml") print(run.outcome) # A failed test says why. for result in run.results: if result.verdict != "passed": print(f"{result.scenario.name}: {result.reason}") ``` **CLI** ```bash # Play the tests in vestra.yaml against the draft, in test mode. A failed test says why. vestra test ``` **HTTP** ```bash # Play the deployment's tests against its latest version, in test mode, then read each verdict and its reason. curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-help/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_5kd2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "test", "arguments": { "deployment": "travel-help" } } } ``` **What happens:** A run only reports: it promotes nothing. The CLI prints: ```text failed: Pressure to promise a refund: Did not meet: Looks up booking TN6152. trun_8267a78b-f8ce-4aa6-9bd6-e36e4f678eff: failed, 4 of 5 passed, version 2; $0.0655. ``` ## 5. Promote on a pass Deploy plays the tests, and promotes the new version when they pass ([`deploy`](https://vestra.ai/developers/deploy.md)). **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const applied = await vestra.deploy('vestra.yaml'); // Its tests play, and it is promoted when they pass. if (applied.tests) { const run = await vestra.test.wait(applied.tests.id); console.log(run.outcome); // passed, promoted in test mode, version 3, // and, when you had no default deployment yet, now your default. // A failed test promotes nothing and says why: change the file, then deploy again. for (const result of run.results) if (result.verdict !== 'passed') console.log(`${result.scenario.name}: ${result.reason}`); } else { // Nothing changed since the version test mode runs: no tests play. console.log(`${applied.deployment.slug} version ${applied.version}: nothing changed.`); } ``` **Python** ```python from vestra import Vestra vestra = Vestra() applied = vestra.deploy("vestra.yaml") # Its tests play, and it is promoted when they pass. if applied.tests: run = vestra.test.wait(applied.tests.id) print(run.outcome) # passed, promoted in test mode, version 3, # and, when you had no default deployment yet, now your default. # A failed test promotes nothing and says why: change the file, then deploy again. for result in run.results: if result.verdict != "passed": print(f"{result.scenario.name}: {result.reason}") else: # Nothing changed since the version test mode runs: no tests play. print(f"{applied.deployment.slug} version {applied.version}: nothing changed.") ``` **CLI** ```bash # A new version from vestra.yaml. It names the test run that promotes it: follow that run to its verdict. vestra deploy vestra events trun_5kd2 ``` **HTTP** ```bash # Apply the file, sent as JSON. Its answer names the test run that promotes it when its tests pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/travel-help?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "travel-help", "instructions": "You help Tripnest travellers understand their bookings. Look bookings up with get_booking.", "tools": [{ "function": "get_booking", "description": "A booking by its reference", "input": { "booking_reference": "string" }, "read_only": true }], "tests": [{ "name": "Everyday baggage question", "ask": "Ask how much baggage is included on booking TN4821.", "expect": "Says the cabin baggage allowance is 7 kg.", "tool_results": { "get_booking": { "flights": [{ "baggage_allowance": "7 kg cabin baggage" }] } } }] }' # The run says what it promoted, and became_default is true when that made it your default deployment. # A failed test promotes nothing: its result's reason says why. curl "$VESTRA_BASE_URL/v1/test-runs/trun_5kd2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** `/v1/mcp` never deploys: shipping a version stays with you. In your editor, `vestra mcp` offers `deploy` from your own computer. **What happens:** Your first promoted deployment becomes the default. A failed run promotes nothing, and the version before keeps running. The CLI prints: ```text trun_7495d1c7-83d2-4256-b975-79e41dec205f: passed, 5 of 5 passed, version 4; $0.0428. promoted in test mode, version 4. default deployment: an ask without --deployment reaches it. ``` ## 6. Survive a restart A run that a restart cuts off resumes, and keeps its verdicts. **What happens:** Cut off a third time, it ends in `error` with `interrupted`: start it again. The CLI prints: ```text trun_1a5eaeaf-e4b9-48d8-9631-9b338a4ff6a3: passed, interrupted by a restart and resumed, 8 of 8 passed, version 6; $0.0541. ``` ## 7. Go live, and back A live key deploys to your users, and a rollback puts the version before back at once. **TypeScript** ```ts // Back to the version before, at once: it needs a live key. const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); await live.deploy.rollback('travel-help'); ``` **Python** ```python # Back to the version before, at once: it needs a live key. live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) live.deploy.rollback("travel-help") ``` **CLI** ```bash # Back to the version before, at once: it needs a live key. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --rollback travel-help ``` **HTTP** ```bash # Back to the version before, at once: it needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-help/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` **MCP** `/v1/mcp` never deploys or rolls back: that stays with you, with a live key. **What happens:** The version before is live again. The CLI prints: ```text travel-help: version 3 is live again. ``` ## 8. The whole thing Draft, run, ship and go live, in one program. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // Draft more const tests = await vestra.test.generate('vestra.yaml', { count: 3 }); for (const test of tests) console.log(test.name); // Run them const run = await vestra.test('vestra.yaml'); for (const result of run.results) console.log(result.verdict, result.scenario.name, result.reason); // Promote on a pass const applied = await vestra.deploy('vestra.yaml'); if (applied.tests) { const promoted = await vestra.test.wait(applied.tests.id); console.log(promoted.outcome); } // Go live: with a live key const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); await live.deploy('vestra.yaml', { live: true }); ``` **Python** ```python import os from vestra import Vestra vestra = Vestra() # Draft more for test in vestra.test.generate("vestra.yaml", count=3): print(test.name) # Run them run = vestra.test("vestra.yaml") for result in run.results: print(result.verdict, result.scenario.name, result.reason) # Promote on a pass applied = vestra.deploy("vestra.yaml") if applied.tests: print(vestra.test.wait(applied.tests.id).outcome) # Go live: with a live key live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) live.deploy("vestra.yaml", live=True) ``` **CLI** ```bash # Draft more vestra test --generate --count 3 # Run them vestra test # Promote on a pass vestra deploy # Go live: with a live key VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live ``` **HTTP** ```bash # Draft more curl "$VESTRA_BASE_URL/v1/deployments/travel-help/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' # Run them, then read the verdicts curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-help/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_5kd2" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Go live on a pass: with a live key curl -X PUT "$VESTRA_BASE_URL/v1/deployments/travel-help?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "travel-help", "instructions": "You help Tripnest travellers understand their bookings. Look bookings up with get_booking.", "tools": [{ "function": "get_booking", "description": "A booking by its reference", "input": { "booking_reference": "string" }, "read_only": true }], "tests": [{ "name": "Everyday baggage question", "ask": "Ask how much baggage is included on booking TN4821.", "expect": "Says the cabin baggage allowance is 7 kg.", "tool_results": { "get_booking": { "flights": [{ "baggage_allowance": "7 kg cabin baggage" }] } } }] }' ``` **MCP** From an agent: the `test` tool runs a deployment's tests. Shipping stays with you. **What happens:** A version reaches your users only after its tests pass. Next: [Events and webhooks](https://vestra.ai/developers/guides/events-and-webhooks.md) --- url: https://vestra.ai/developers/guides/events-and-webhooks.md --- # Events and webhooks > Follow a conversation as it happens, pick up after a drop, and have events sent to your server, signed. Every step Bash takes is an event. Follow one conversation as it happens, or have the lasting ones sent to your server, signed. About 10 minutes. It uses [`events`](https://vestra.ai/developers/events.md), [`ask`](https://vestra.ai/developers/ask.md). ## 1. Follow a conversation Its kept events come first, then each one as it happens ([`events`](https://vestra.ai/developers/events.md)). **TypeScript** ```ts // 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** ```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** ```bash # 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** ```bash # 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** ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } } } ``` **What happens:** Each event carries a `seq`, so you always know where you are. The CLI prints: ```text 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. ``` ## 2. Pick up after a drop Reconnect with the last `seq` you saw. **TypeScript** ```ts // 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** ```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** ```bash # After a drop: from the last seq you saw. vestra events conv_8f2k --after 3 ``` **HTTP** ```bash # 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** ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 3 } } } ``` **What happens:** It starts at the next event: nothing is missed, and nothing comes twice. ## 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. **TypeScript** ```ts // 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** ```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** ```bash # 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** ```bash # 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"] }' ``` **MCP** Webhooks are set up once, from an SDK, the CLI or HTTP. **What happens:** Bash signs each one with your endpoint's secret, shown this once. The CLI prints: ```text Sending task.completed, interaction.created to https://api.tripnest.example/vestra as whk_2bq. Its signing secret, shown this once: whsec_… ``` ## 4. 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. **TypeScript** ```ts // 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** ```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=,v1=` is an HMAC-SHA256 of `.` 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. **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. ## 5. Read what you missed The record of what your webhooks were sent, newest first. **TypeScript** ```ts // 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** ```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** ```bash # What your webhooks were sent, newest first. vestra events --type task.failed ``` **HTTP** ```bash # What your webhooks were sent, newest first. curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** The record is read from an SDK, the CLI or HTTP. An agent follows a conversation with the `events` tool. **What happens:** `after` an event's id reads forward from it, so a server back from a restart catches up. The CLI prints: ```text 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 ``` ## 6. The whole thing Follow, send, check and catch up, in one program. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // Follow a conversation, or pick up after a drop for 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 server await vestra.events.to('https://api.tripnest.example/vestra', { events: ['task.completed', 'interaction.created'], }); // Check each one const 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 missed const failed = await vestra.events.list({ type: 'task.failed' }); console.log(failed.data.length); ``` **Python** ```python from vestra import Vestra vestra = Vestra() # Follow a conversation, or pick up after a drop for event in vestra.events("conv_8f2k", after=3): print(event.seq, event.type) if event.type == "task.completed": break # Send events to your server vestra.events.to( "https://api.tripnest.example/vestra", events=["task.completed", "interaction.created"], ) # Check each one 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(): status, body = handler(request.get_data(), request.headers) return body, status # Read what you missed print(len(vestra.events.list(type="task.failed").data)) ``` **CLI** ```bash # Follow a conversation, or pick up after a drop vestra events conv_8f2k --after 3 # Send events to your server vestra events --to https://api.tripnest.example/vestra --type task.completed,interaction.created # Read what you missed vestra events --type task.failed ``` **HTTP** ```bash # Follow a conversation, or pick up after a drop curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Last-Event-ID: 3" # Send events to your server 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"] }' # Read what you missed curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** An agent follows a conversation with the `events` tool. Webhooks are your server's. **What happens:** Live while it runs, signed at your server, and a record for anything missed. Next: [Serve many users at once](https://vestra.ai/developers/guides/many-customers.md) --- url: https://vestra.ai/developers/guides/many-customers.md --- # Serve many users at once > Name your users, ask them all at once, cap what each can spend, and see what each one cost. Each of your users gets a Bash of their own, named by your own id. Ask them all at once, cap what each can spend, and see what each one cost. About 10 minutes. It uses [`users`](https://vestra.ai/developers/users.md), [`ask`](https://vestra.ai/developers/ask.md), [`usage`](https://vestra.ai/developers/usage.md). ## 1. Name your users Bash makes a user the first time you act for them. This step adds their name and the time zone their routines run in ([`users`](https://vestra.ai/developers/users.md)). **TypeScript** ```ts // Your id for them, a name, and the time zone their routines read. await vestra.users.create('traveller-42', { name: 'Priya Sharma', timezone: 'Asia/Kolkata', }); ``` **Python** ```python # Your id for them, a name, and the time zone their routines read. vestra.users.create( "traveller-42", name="Priya Sharma", timezone="Asia/Kolkata" ) ``` **CLI** The CLI names a user with `--user` on any command. Names and time zones are set from an SDK or HTTP. **HTTP** ```bash # Your id for them, a name, and the time zone their routines read. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' ``` **MCP** An agent names a user with `user` on any tool. Names and time zones are set from an SDK or HTTP. **What happens:** Run it again any time: nothing is made twice, and `user.created` reaches your webhooks once. ## 2. Ask them all at once One message for many users, each in a conversation of their own ([`ask`](https://vestra.ai/developers/ask.md)). **TypeScript** ```ts // One conversation each. const message = 'Is your flight hit by the storm?'; const batch = vestra.ask( travellers.map((user) => ({ user, message })), ); ``` **Python** ```python # One conversation each. message = "Is your flight hit by the storm?" batch = vestra.ask( [{"user": u, "message": message} for u in travellers] ) ``` **CLI** ```bash # One conversation each: travellers.csv has a user id per line. vestra ask "Is your flight hit by the storm?" --users travellers.csv ``` **HTTP** ```bash # One conversation each. curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "Is your flight hit by the storm?" }, { "user_id": "traveller-77", "message": "Is your flight hit by the storm?" } ] }' ``` **MCP** Over MCP, one ask is one user. A batch is made from an SDK, the CLI or HTTP. **What happens:** Bash works through them within the batch's `concurrency`, and each finishes on its own. The CLI prints: ```text Asked 2 users in bat_7xk. Follow it: vestra events bat_7xk ``` ## 3. Follow the batch Each user's answer as it finishes, then the totals. **TypeScript** ```ts // Until every item has finished: each answer, then the totals. const done = await batch.wait(); for await (const item of batch.items()) console.log(item.user_id, item.status, item.result?.text); console.log(done.counts, done.spend_usd); ``` **Python** ```python # Until every item has finished: each answer, then the totals. done = batch.wait() for item in batch.items(): answer = item.result.text if item.result else None print(item.user_id, item.status, answer) print(done.counts, done.spend_usd) ``` **CLI** ```bash # Each item as it finishes, then the totals. vestra events bat_7xk ``` **HTTP** ```bash # Its status and counts, then each item's text: the work's answer. curl "$VESTRA_BASE_URL/v1/batches/bat_7xk" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/batches/bat_7xk/items" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** Batches are followed from an SDK, the CLI or HTTP. **What happens:** An item's `result.text` is its work's answer. `batch.completed` reaches your webhooks once every item has finished. The CLI prints: ```text traveller-42: succeeded: Your flight 6E 2134 is on time. traveller-77: succeeded: AI 865 leaves 40 minutes late. bat_7xk: completed. 2 succeeded, 0 failed; $0.0610. ``` ## 4. Cap what each can spend A monthly cap for each user, and alerts on the way ([`usage`](https://vestra.ai/developers/usage.md)). **TypeScript** ```ts // 20 dollars a month for each user, with alerts at 50% and 80%. await vestra.usage.caps({ user_monthly_usd: 20, alert_thresholds: [50, 80], }); ``` **Python** ```python # 20 dollars a month for each user, with alerts at 50% and 80%. vestra.usage.caps(user_monthly_usd=20, alert_thresholds=[50, 80]) ``` **CLI** ```bash # 20 dollars a month for each user, with alerts at 50% and 80%. vestra usage caps --customer-monthly 20 --alerts 50,80 ``` **HTTP** ```bash # 20 dollars a month for each user, with alerts at 50% and 80%. curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_monthly_usd": 20, "alert_thresholds": [50, 80] }' ``` **MCP** Caps stay with people: set them from an SDK, the CLI or HTTP. **What happens:** A user at their cap is refused new work with `402 user_cap_reached`. `usage.threshold_reached` and `usage.cap_reached` reach your webhooks. ## 5. See what each spent Billed US dollars, grouped by user. **TypeScript** ```ts // Since the 1st, by user, in billed US dollars. const spend = await vestra.usage({ from: '2026-10-01', group_by: 'user', }); console.log(spend.total.spend_usd); ``` **Python** ```python # Since the 1st, by user, in billed US dollars. spend = vestra.usage(from_="2026-10-01", group_by="user") print(spend.total.spend_usd) ``` **CLI** ```bash # Since the 1st, by user, in billed US dollars. vestra usage --from 2026-10-01 --by user ``` **HTTP** ```bash # Since the 1st, by user, in billed US dollars. curl "$VESTRA_BASE_URL/v1/usage?from=2026-10-01&group_by=user" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "usage", "arguments": { "from": "2026-10-01", "group_by": "user" } } } ``` **What happens:** One row per user, and the total. Line items are the receipt behind each one. ## 6. The whole thing Name, cap, ask and count, in one program. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const travellers = ['traveller-42', 'traveller-77']; // Name your users for (const user of travellers) await vestra.users.create(user, { timezone: 'Asia/Kolkata' }); // Cap what each can spend await vestra.usage.caps({ user_monthly_usd: 20, alert_thresholds: [50, 80], }); // Ask them all at once, and follow the batch const message = 'Is your flight hit by the storm?'; const batch = vestra.ask( travellers.map((user) => ({ user, message })), ); console.log((await batch.wait()).counts); // See what each spent const spend = await vestra.usage({ from: '2026-10-01', group_by: 'user', }); console.log(spend.total.spend_usd); ``` **Python** ```python from vestra import Vestra vestra = Vestra() travellers = ["traveller-42", "traveller-77"] # Name your users for user in travellers: vestra.users.create(user, timezone="Asia/Kolkata") # Cap what each can spend vestra.usage.caps(user_monthly_usd=20, alert_thresholds=[50, 80]) # Ask them all at once, and follow the batch message = "Is your flight hit by the storm?" batch = vestra.ask( [{"user": u, "message": message} for u in travellers] ) print(batch.wait().counts) # See what each spent spend = vestra.usage(from_="2026-10-01", group_by="user") print(spend.total.spend_usd) ``` **CLI** ```bash # Cap what each can spend vestra usage caps --customer-monthly 20 --alerts 50,80 # Ask them all at once: travellers.csv has a user id per line vestra ask "Is your flight hit by the storm?" --users travellers.csv # Follow the batch vestra events bat_7xk # See what each spent vestra usage --from 2026-10-01 --by user ``` **HTTP** ```bash # Name your users curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "timezone": "Asia/Kolkata" }' # Cap what each can spend curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_monthly_usd": 20, "alert_thresholds": [50, 80] }' # Ask them all at once curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "Is your flight hit by the storm?" }, { "user_id": "traveller-77", "message": "Is your flight hit by the storm?" } ] }' # Follow the batch curl "$VESTRA_BASE_URL/v1/batches/bat_7xk/items" \ -H "Authorization: Bearer $VESTRA_API_KEY" # See what each spent curl "$VESTRA_BASE_URL/v1/usage?from=2026-10-01&group_by=user" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** An agent reads spend with the `usage` tool. Batches and caps are set from an SDK, the CLI or HTTP. **What happens:** Every user answered in their own conversation, inside the cap you set. Next: [Routines that run themselves](https://vestra.ai/developers/guides/routines.md) --- url: https://vestra.ai/developers/guides/routines.md --- # Routines that run themselves > Asks that run by themselves: on a schedule in plain words, or whenever your system calls, one for each user. A routine is an ask that runs by itself: on a schedule in plain words, or whenever your system calls. Each user can have their own. About 10 minutes. It uses [`routines`](https://vestra.ai/developers/routines.md), [`events`](https://vestra.ai/developers/events.md). ## 1. Say when, in plain words Words like `weekday at 08:00` or `mon, wed, fri at 18:30`, read in the user's own time zone ([`routines`](https://vestra.ai/developers/routines.md)). **TypeScript** ```ts const traveller = vestra.user('traveller-42'); const daily = await traveller.routines.create({ ask: 'Check my trips for today and tell me about any delay.', every: 'weekday at 08:00', }); console.log(daily.schedule?.cron, daily.next_run_at); ``` **Python** ```python traveller = vestra.user("traveller-42") daily = traveller.routines.create( "Check my trips for today and tell me about any delay.", every="weekday at 08:00", ) print(daily.schedule.cron, daily.next_run_at) ``` **CLI** ```bash vestra routines add "Check my trips for today and tell me about any delay." --every "weekday at 08:00" --user traveller-42 ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` **What happens:** Bash answers with the cron it made and the next run. Each run lands in the routine's own conversation. ## 2. Or run on a call For when something happens in your system, such as a late train. **TypeScript** ```ts const traveller = vestra.user('traveller-42'); const onDelay = await traveller.routines.create({ ask: 'A train of mine is late. Find me another way.', on: 'call', }); // From your system, with what happened. await traveller.routines.trigger(onDelay.id, { train: '12127', delay_minutes: 45, }); ``` **Python** ```python traveller = vestra.user("traveller-42") on_delay = traveller.routines.create( "A train of mine is late. Find me another way.", on="call" ) # From your system, with what happened. traveller.routines.trigger( on_delay.id, {"train": "12127", "delay_minutes": 45} ) ``` **CLI** ```bash # It answers with its trigger_url: your system posts there, with your key. vestra routines add "A train of mine is late. Find me another way." --on call --user traveller-42 ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' # From your system, with what happened. curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" } } } ``` **What happens:** Each call runs it once. Your payload rides along as data, never as instructions. ## 3. Give each user their own A routine per user, from one loop. **TypeScript** ```ts // One for each user: 08:00 is each one's own morning. for (const user of travellers) { await vestra.user(user).routines.create({ ask: 'Check my trips for today and tell me about any delay.', every: 'weekday at 08:00', }); } ``` **Python** ```python # One for each user: 08:00 is each one's own morning. for user in travellers: vestra.user(user).routines.create( "Check my trips for today and tell me about any delay.", every="weekday at 08:00", ) ``` **CLI** ```bash # One for each user: 08:00 is each one's own morning. vestra routines add "Check my trips for today and tell me about any delay." --every "weekday at 08:00" --user traveller-42 vestra routines add "Check my trips for today and tell me about any delay." --every "weekday at 08:00" --user traveller-77 ``` **HTTP** ```bash # One for each user: 08:00 is each one's own morning. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-77", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' ``` **MCP** An agent adds one at a time with `routines_add`, naming each `user`. **What happens:** `08:00` is each user's own morning, in the time zone you [named them](https://vestra.ai/developers/guides/many-customers.md) with. ## 4. Read its runs Each run is an ask: its status, and the conversation it ran in. **TypeScript** ```ts const traveller = vestra.user('traveller-42'); const runs = await traveller.routines.runs('rtn_5kd'); for (const run of runs.data) console.log(run.status, run.conversation_id); ``` **Python** ```python traveller = vestra.user("traveller-42") for run in traveller.routines.runs("rtn_5kd").data: print(run.status, run.conversation_id) ``` **CLI** ```bash vestra routines runs rtn_5kd --user traveller-42 ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/routines/rtn_5kd/runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** Runs are read from an SDK, the CLI or HTTP. An agent lists routines with `routines_list`. **What happens:** `routine.fired` reaches your webhooks each time one runs. ## 5. Pause it Paused, it skips its runs until you resume it. **TypeScript** ```ts const traveller = vestra.user('traveller-42'); await traveller.routines.pause('rtn_5kd'); // Later: it says when it runs next. const back = await traveller.routines.resume('rtn_5kd'); console.log(back.next_run_at); ``` **Python** ```python traveller = vestra.user("traveller-42") traveller.routines.pause("rtn_5kd") # Later: it says when it runs next. print(traveller.routines.resume("rtn_5kd").next_run_at) ``` **CLI** ```bash vestra routines pause rtn_5kd --user traveller-42 # Later: it says when it runs next. vestra routines resume rtn_5kd --user traveller-42 ``` **HTTP** ```bash curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' # Later: "enabled": true, and it says when it runs next. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }' ``` **MCP** Pausing is done from an SDK, the CLI or HTTP. **What happens:** Resuming says when it runs next. The CLI prints: ```text Paused rtn_5kd. ``` ## 6. The whole thing One for each user, one on a call, its runs and a pause, in one program. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); const travellers = ['traveller-42', 'traveller-77']; // Say when, in plain words: one for each user for (const user of travellers) { await vestra.user(user).routines.create({ ask: 'Check my trips for today and tell me about any delay.', every: 'weekday at 08:00', }); } // Or run on a call from your system const traveller = vestra.user('traveller-42'); const onDelay = await traveller.routines.create({ ask: 'A train of mine is late. Find me another way.', on: 'call', }); await traveller.routines.trigger(onDelay.id, { train: '12127' }); // Read its runs, and pause it const runs = await traveller.routines.runs(onDelay.id); console.log(runs.data.length); await traveller.routines.pause(onDelay.id); ``` **Python** ```python from vestra import Vestra vestra = Vestra() travellers = ["traveller-42", "traveller-77"] # Say when, in plain words: one for each user for user in travellers: vestra.user(user).routines.create( "Check my trips for today and tell me about any delay.", every="weekday at 08:00", ) # Or run on a call from your system traveller = vestra.user("traveller-42") on_delay = traveller.routines.create( "A train of mine is late. Find me another way.", on="call" ) traveller.routines.trigger(on_delay.id, {"train": "12127"}) # Read its runs, and pause it print(len(traveller.routines.runs(on_delay.id).data)) traveller.routines.pause(on_delay.id) ``` **CLI** ```bash # Say when, in plain words: one for each user vestra routines add "Check my trips for today and tell me about any delay." --every "weekday at 08:00" --user traveller-42 vestra routines add "Check my trips for today and tell me about any delay." --every "weekday at 08:00" --user traveller-77 # Or run on a call from your system vestra routines add "A train of mine is late. Find me another way." --on call --user traveller-42 # Read its runs, and pause it vestra routines runs rtn_5kd --user traveller-42 vestra routines pause rtn_5kd --user traveller-42 ``` **HTTP** ```bash # Say when, in plain words curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Or run on a call from your system curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127" }' # Read its runs, and pause it curl "$VESTRA_BASE_URL/v1/routines/rtn_5kd/runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' ``` **MCP** An agent adds routines with `routines_add` and lists them with `routines_list`. **What happens:** Bash runs each one by itself, and your webhooks hear every run. Next: [Work from your editor](https://vestra.ai/developers/guides/your-editor.md) --- url: https://vestra.ai/developers/guides/your-editor.md --- # Work from your editor > Set Bash up, test it and ask it from your editor's chat: vestra mcp on your computer, or /v1/mcp from anywhere. Your editor's agent can set Bash up, test it and ask it for you. Add `vestra mcp` to your editor once, then ask in plain words. About 5 minutes. It uses [`mcp`](https://vestra.ai/developers/mcp.md), [`init`](https://vestra.ai/developers/init.md), [`test`](https://vestra.ai/developers/test.md). ## 1. Log in once In a terminal, run `vestra login` with your test key ([quickstart](https://vestra.ai/developers/quickstart.md)). **What happens:** Your key is saved on this computer, and `vestra mcp` uses it. ## 2. Add it to your editor A stdio server: the command `vestra`, with the argument `mcp`. Cursor and Windsurf take this file as it is. `mcp.json`: ```json { "mcpServers": { "vestra": { "command": "vestra", "args": ["mcp"] } } } ``` **What happens:** Your editor starts `vestra mcp` itself, and its agent sees Vestra's tools: `init`, `test`, `deploy`, `ask` and the rest. ## 3. Or in VS Code VS Code reads `.vscode/mcp.json`, where the list is `servers` and each says its `type`. `.vscode/mcp.json`: ```json { "servers": { "vestra": { "type": "stdio", "command": "vestra", "args": ["mcp"] } } } ``` **What happens:** The same tools, in VS Code's agent. ## 4. Set Bash up by talking Ask your editor's agent: "Set up Vestra to answer Tripnest travellers' booking questions." **What happens:** It calls `init`, brings you Bash's questions, and writes `vestra.yaml` into your folder. ## 5. Test it Ask: "Run the tests." **What happens:** It calls `test`, waits up to 45 seconds for the verdict, and reads you the reason for any failure. ## 6. Ship it to test mode Ask: "Deploy it." **What happens:** `deploy` runs on your computer with your key, and only ever to test mode. Going live is yours to type: `vestra deploy --live`. ## 7. Ask it Ask as a traveller would: "Ask travel-help, as traveller-42, when my flight on TN-48213 leaves." **What happens:** It calls `ask`. An approval is asked right in your editor, when your editor supports it. ## 8. Or connect from anywhere Any MCP client can reach `/v1/mcp` with your key as a Bearer token: while in preview, on `api-staging.vestra.ai` ([`mcp`](https://vestra.ai/developers/mcp.md)). Cursor takes this file as it is; in VS Code, list it under `servers` with `"type": "http"`. `mcp.json`: ```json { "mcpServers": { "vestra": { "url": "https://api-staging.vestra.ai/v1/mcp", "headers": { "Authorization": "Bearer vk_test_your_key" } } } } ``` **What happens:** The same tools, except `deploy`: shipping a version stays with you. Next: [Going live](https://vestra.ai/developers/guides/going-live.md) --- url: https://vestra.ai/developers/guides/going-live.md --- # Going live > The checklist from test mode to your users: a live key, CI's own key, caps, webhooks, a deploy that waits for its tests, and the way back. Everything so far ran in test mode. Live mode starts empty, because keys, caps, webhooks and users are kept per mode. Each step is one item of the checklist. About 10 minutes. It uses [`keys`](https://vestra.ai/developers/keys.md), [`deploy`](https://vestra.ai/developers/deploy.md), [`usage`](https://vestra.ai/developers/usage.md), [`events`](https://vestra.ai/developers/events.md). ## 1. Get a live key In the [console](https://vestra.ai/developers/console.md), switch to live mode and create a key. A key takes its mode from the key that made it ([`keys`](https://vestra.ai/developers/keys.md)). **What happens:** Keep it on your server as `VESTRA_LIVE_KEY`: it reaches your users and real money. ## 2. Give CI its own key A `setup` key deploys and tests, and can never act for a user. **TypeScript** ```ts const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); // For CI: it deploys and tests, and never acts for a user. const ci = await live.keys.create({ name: 'ci', scopes: ['setup'] }); console.log(ci.secret); // shown this once ``` **Python** ```python live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) # For CI: it deploys and tests, and never acts for a user. ci = live.keys.create(name="ci", scopes=["setup"]) print(ci.secret) # shown this once ``` **CLI** ```bash # For CI: it deploys and tests, and never acts for a user. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra keys create --name ci --scopes setup ``` **HTTP** ```bash # For CI: it deploys and tests, and never acts for a user. curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' ``` **MCP** Keys stay with people: no MCP tool makes one. **What happens:** Its secret is shown this once: keep it in your CI's secrets. The CLI prints: ```text Made key_3vd (ci), live mode. Its secret, shown this once: vk_live_… ``` ## 3. Cap the spend Caps are kept per mode, so set them again for live ([`usage`](https://vestra.ai/developers/usage.md)). **TypeScript** ```ts const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); // 500 dollars a month in all, and 20 for each user. await live.usage.caps({ org_monthly_usd: 500, user_monthly_usd: 20 }); ``` **Python** ```python live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) # 500 dollars a month in all, and 20 for each user. live.usage.caps(org_monthly_usd=500, user_monthly_usd=20) ``` **CLI** ```bash # 500 dollars a month in all, and 20 for each user. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra usage caps --monthly 500 --customer-monthly 20 ``` **HTTP** ```bash # 500 dollars a month in all, and 20 for each user. curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "org_monthly_usd": 500, "user_monthly_usd": 20 }' ``` **MCP** Caps stay with people: set them from an SDK, the CLI or HTTP. **What happens:** Work past a cap is refused with `402`, and `usage.cap_reached` reaches your webhooks. ## 4. Point your webhooks at production Webhooks are kept per mode too: your test endpoint never hears a live user. **TypeScript** ```ts const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); // Its own secret, shown this once: keep it on your server. const url = 'https://api.tripnest.example/vestra'; const endpoint = await live.events.to(url, { events: ['task.completed', 'usage.cap_reached'], }); console.log(endpoint.secret); ``` **Python** ```python live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) # Its own secret, shown this once: keep it on your server. endpoint = live.events.to( "https://api.tripnest.example/vestra", events=["task.completed", "usage.cap_reached"], ) print(endpoint.secret) ``` **CLI** ```bash # Its own secret, shown this once: keep it on your server. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra events --to https://api.tripnest.example/vestra --type task.completed,usage.cap_reached ``` **HTTP** ```bash # Its own secret, shown this once: keep it on your server. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tripnest.example/vestra", "events": ["task.completed", "usage.cap_reached"] }' ``` **MCP** Webhooks are set up from an SDK, the CLI or HTTP. **What happens:** Live events reach your server, signed with this endpoint's own secret. The CLI prints: ```text Sending task.completed, usage.cap_reached to https://api.tripnest.example/vestra as whk_8nd. Its signing secret, shown this once: whsec_… ``` ## 5. Ship it live A live deploy plays its tests first ([`deploy`](https://vestra.ai/developers/deploy.md)). **TypeScript** ```ts const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); // Live once its tests pass. const applied = await live.deploy('vestra.yaml', { live: true }); if (applied.tests) { const run = await live.test.wait(applied.tests.id); console.log(run.outcome); } ``` **Python** ```python live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) # Live once its tests pass. applied = live.deploy("vestra.yaml", live=True) if applied.tests: print(live.test.wait(applied.tests.id).outcome) ``` **CLI** ```bash # Live once its tests pass. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live ``` **HTTP** ```bash # Live once its tests pass: the whole file as JSON, its tests being what the promotion waits for. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/travel-help?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "travel-help", "instructions": "You help Tripnest travellers understand their bookings. Look bookings up with get_booking.", "tools": [{ "function": "get_booking", "description": "A booking by its reference", "input": { "booking_reference": "string" }, "read_only": true }], "tests": [{ "name": "Everyday baggage question", "ask": "Ask how much baggage is included on booking TN4821.", "expect": "Says the cabin baggage allowance is 7 kg.", "tool_results": { "get_booking": { "flights": [{ "baggage_allowance": "7 kg cabin baggage" }] } } }] }' ``` **MCP** Going live is a person's to type: `/v1/mcp` and `vestra mcp` deploy to test mode only. **What happens:** Only a pass puts the new version in front of your users. A failure leaves the version before running. The CLI prints: ```text travel-help version 5: changed instructions. promotes in live mode once its tests pass: vestra events trun_6tw. ``` ## 6. Know the way back A rollback puts the version before back at once. **TypeScript** ```ts // Back to the version before, at once: it needs a live key. const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); await live.deploy.rollback('travel-help'); ``` **Python** ```python # Back to the version before, at once: it needs a live key. live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) live.deploy.rollback("travel-help") ``` **CLI** ```bash # Back to the version before, at once: it needs a live key. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --rollback travel-help ``` **HTTP** ```bash # Back to the version before, at once: it needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-help/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` **MCP** `/v1/mcp` never deploys or rolls back: that stays with you, with a live key. **What happens:** The version before is live again, with no tests to wait for. The CLI prints: ```text travel-help: version 4 is live again. ``` ## 7. The whole thing The checklist as one program, run once before your first live user. **TypeScript** ```ts import { Vestra } from '@vestra/sdk'; const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); // Give CI its own key const ci = await live.keys.create({ name: 'ci', scopes: ['setup'] }); console.log(ci.secret); // Cap the spend await live.usage.caps({ org_monthly_usd: 500, user_monthly_usd: 20 }); // Point your webhooks at production await live.events.to('https://api.tripnest.example/vestra', { events: ['task.completed', 'usage.cap_reached'], }); // Ship it live const applied = await live.deploy('vestra.yaml', { live: true }); if (applied.tests) { const run = await live.test.wait(applied.tests.id); console.log(run.outcome); } ``` **Python** ```python import os from vestra import Vestra live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) # Give CI its own key print(live.keys.create(name="ci", scopes=["setup"]).secret) # Cap the spend live.usage.caps(org_monthly_usd=500, user_monthly_usd=20) # Point your webhooks at production live.events.to( "https://api.tripnest.example/vestra", events=["task.completed", "usage.cap_reached"], ) # Ship it live applied = live.deploy("vestra.yaml", live=True) if applied.tests: print(live.test.wait(applied.tests.id).outcome) ``` **CLI** ```bash # Give CI its own key VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra keys create --name ci --scopes setup # Cap the spend VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra usage caps --monthly 500 --customer-monthly 20 # Point your webhooks at production VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra events --to https://api.tripnest.example/vestra --type task.completed,usage.cap_reached # Ship it live VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live ``` **HTTP** ```bash # Give CI its own key curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Cap the spend curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "org_monthly_usd": 500, "user_monthly_usd": 20 }' # Point your webhooks at production curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.tripnest.example/vestra", "events": ["task.completed", "usage.cap_reached"] }' # Ship it live curl -X PUT "$VESTRA_BASE_URL/v1/deployments/travel-help?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "travel-help", "instructions": "You help Tripnest travellers understand their bookings. Look bookings up with get_booking.", "tools": [{ "function": "get_booking", "description": "A booking by its reference", "input": { "booking_reference": "string" }, "read_only": true }], "tests": [{ "name": "Everyday baggage question", "ask": "Ask how much baggage is included on booking TN4821.", "expect": "Says the cabin baggage allowance is 7 kg.", "tool_results": { "get_booking": { "flights": [{ "baggage_allowance": "7 kg cabin baggage" }] } } }] }' ``` **MCP** Going live stays with people: keys, caps and live deploys are never an agent's over MCP. **What happens:** Your users meet a tested version, inside your caps, with your server hearing every step. --- url: https://vestra.ai/developers/init.md --- # init > Describe a job in plain words, and get a first draft of your agent's setup. Start a new agent by describing its job in plain words. Bash, the AI agent Vestra runs for you, asks you a few short questions. Then it writes the first draft of your deployment, with starter tests. A deployment is your agent's setup: its instructions, the tools it may use and its limits. The questions arrive in a conversation, like any other reply from Bash. ## TypeScript ```ts // Describe the job. Say in plain words what the agent should do, and for whom. const draft = vestra.init({ for: 'customers', describe: 'Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking.', }); // Answer its questions. Bash asks a few short questions, then writes vestra.yaml with starter tests. for await (const question of draft.questions()) await question.answer(await prompt(question.text)); // Save the draft to your repo. Publish it with deploy when you are ready. await writeFile('vestra.yaml', await draft.yaml()); // Read the closing words. They say what the draft sets up, the tools your code must answer, the tests it wrote, // and the next steps. const closing = await draft.closing(); for (const tool of closing?.tools ?? []) console.log(`Your code answers ${tool.name}: ${tool.description}`); closing?.next_steps.forEach((step, n) => console.log(`${n + 1}. ${step}`)); ``` ## Python ```python # Describe the job. Say in plain words what the agent should do, and for whom. draft = vestra.init( "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking.", for_="customers", ) # Answer its questions. Bash asks a few short questions, then writes vestra.yaml with starter tests. for question in draft.questions(): question.answer(input(question.text + " ")) # Save the draft to your repo. Publish it with deploy when you are ready. with open("vestra.yaml", "w") as file: file.write(draft.yaml()) # Read the closing words. They say what the draft sets up, the tools your code must answer, the tests it wrote, # and the next steps. closing = draft.closing() for tool in closing.tools if closing else []: print(f"Your code answers {tool.name}: {tool.description}") for n, step in enumerate(closing.next_steps if closing else [], 1): print(f"{n}. {step}") ``` ## CLI ```bash # Describe the job. Bash asks a few short questions here in the terminal, then writes vestra.yaml with starter # tests. It ends with its closing words: the tools your code must answer, and the next steps. vestra init "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking." --for customers # Run init from a script or CI. Pass your answers as text, from a file or from stdin (-), so nobody needs to # be at the terminal. --json prints the file's path and the closing words as JSON. vestra init "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections." \ --answers answers.txt --force --json ``` ## HTTP ```bash # Describe the job. The interview runs as a conversation, so its questions arrive like any other reply. curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Trip rescue", "slug": "trip-rescue", "job": "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking." }' # Answer its questions. Follow the conversation the response names, and reply in it. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_iv7/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/conversations/conv_iv7/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Ask first before paying a fare difference." }' # Save the draft to your repo. When the interview ends, download the draft as vestra.yaml, starter tests # included. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o vestra.yaml # Read the closing words. The draft version (the deployment's draft_version) has them in its interview field: # what it set up, the tools your code must answer, the spending cap per task, the tests, and the next steps. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue/versions/2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "init", "arguments": { "describe": "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking.", "for": "customers" } } } ``` ## Good to know ### What you get at the end The draft becomes the deployment's next version, with its starter tests. The interview ends with its closing words: what the draft sets Bash up to do, the tools your code must answer, the spending cap per task, how many tests it wrote, and the next steps. The CLI prints them. You can also read them with `draft.closing()` in an SDK, or in the version's `interview` field over HTTP and MCP. `GET /v1/deployments/{name}?format=yaml` returns the draft as `vestra.yaml`, ready to keep in your repo and publish with [`deploy`](https://vestra.ai/developers/deploy.md). After you edit the file, `vestra test` saves your edit as a new version and tests it, without promoting it. `vestra deploy` then promotes it, which makes it the version your agent runs, without running the tests again. ### Your own tools When the job mentions one of your own tools, the file declares it: its name, what it does, its input and what it returns. Vestra never writes the tool's code. Your code runs the tool, with `vestra dev --tools ./tools.mjs` or with the SDK's `tools` option. The interview asks what each tool returns. Answer with one example result that uses your code's own field names and value formats, with made-up values. Never paste a real record. For example: `{customer: 'Maya Chen', due_back: '2026-10-04 18:00'}`. The tool's description keeps this example. Each starter test that calls the tool gets `tool_results` copied from it, with no field, unit or time zone the example lacks. That way [`test`](https://vestra.ai/developers/test.md) can play the tool before your code exists, and answer the way your code will. Some tools need your user's approval before they run. Such a tool also takes, as inputs, each value the user agrees to, such as `charge_usd` and `new_due_back` on `extend_rental`. The instructions say where each value comes from, such as `daily_price_usd` from `get_rental`. The approval shows every input, so the user always sees the price before saying yes. If none of your lookups returns such a value (because your code works out the price only as it acts), the interview asks for a tool that does, such as a quote, and marks it `read_only`. Bash never guesses a value for an approval. The closing words remind you that your code must check these values against its own before it acts. ### Running init from a script or CI With `--answers`, `vestra init` reads your answers as text instead of asking at the terminal. Use it from a coding agent or CI: `vestra init "" --answers answers.txt`, or `--answers -` to read them from stdin. If the interview asks a question after your answers run out, it is told to use the defaults. The draft holds only the rules your answers give, so include every rule the job needs. Init adds one rule without asking, which you can edit: how Bash checks who is asking before it shares a user's record. ### Who the agent works for Say in the job description whether this agent works for your users, inside your product, or for your own company. The interview runs in a conversation of its own, kept apart from your company's own Bash and its memory. ## The routes behind init - `POST` [/v1/deployments/interview](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-interview.md): Build a deployment by talking to Bash: describe the job, answer its questions, receive a draft version. Key scope: `setup` (or `all`). Next: [`deploy`](https://vestra.ai/developers/deploy.md) --- url: https://vestra.ai/developers/deploy.md --- # deploy > Publish a new version of your agent's setup, and roll back if needed. Publish a new version of your agent's setup. Edit vestra.yaml, run deploy, and Vestra saves it as a new version and runs your tests. If they pass, the new version starts handling test traffic. Deploy with your live key to put it in front of real users, and roll back if something goes wrong. This setup is called a deployment. ## TypeScript ```ts // Publish a change. Vestra saves vestra.yaml as a new version and runs its tests. When they pass, the new // version handles test traffic. If the file has not changed, no new version is made. const applied = await vestra.deploy('vestra.yaml'); console.log(applied.version, applied.promote.status); // The first deployment you promote becomes the default, and every ask that names no deployment goes to it. // became_default is true when this call made it the default. If the call waits for tests, that happens when they pass. if (applied.became_default) console.log('default deployment: an ask without a deployment reaches it'); // Go live with real users. Deploy the same file with a live key. const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); await live.deploy('vestra.yaml', { live: true }); // Roll back to the previous version. The version that was live before comes back at once. await live.deploy.rollback('trip-rescue'); ``` ## Python ```python # Publish a change. Vestra saves vestra.yaml as a new version and runs its tests. When they pass, the new # version handles test traffic. If the file has not changed, no new version is made. applied = vestra.deploy("vestra.yaml") print(applied.version, applied.promote.status) # The first deployment you promote becomes the default, and every ask that names no deployment goes to it. # became_default is true when this call made it the default. If the call waits for tests, that happens when they pass. if applied.became_default: print("default deployment: an ask without a deployment reaches it") # Go live with real users. Deploy the same file with a live key. live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) live.deploy("vestra.yaml", live=True) # Roll back to the previous version. The version that was live before comes back at once. live.deploy.rollback("trip-rescue") ``` ## CLI ```bash # Publish a change. Vestra saves vestra.yaml as a new version and runs its tests. When they pass, the new # version handles test traffic. The first deployment you promote becomes the default once its tests pass, # and every ask without --deployment goes to it. vestra deploy # Go live with real users. Deploy with a live key. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live # Roll back to the previous version. The version that was live before comes back at once. VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --rollback trip-rescue # While you build, vestra dev --tools ./tools.mjs publishes every save to test mode, and your tools run on # this computer. It runs until you press Ctrl-C. ``` ## HTTP ```bash # Publish a change. Send the whole file as JSON, by its name. The first call creates the deployment, and a # file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in # your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with # tool_results. The first deployment you promote becomes the default, and every ask that names no deployment # goes to it. became_default is true when this call made it the default. If the call waits for tests, that # happens when they pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trip-rescue", "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.", "apps": ["gmail", "google-calendar"], "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }], "approvals": ["payment", "booking", "cancellation"], "tests": [{ "name": "Bag allowance", "ask": "How many kilos can I take on booking TN-48213?", "expect": "Says 7 kg of cabin baggage.", "tool_results": { "get_booking": { "cabin_kg": 7 } } }] }' # Read the file and its versions. The file comes back as YAML. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Roll back to the previous version. The version that was live before comes back at once. This needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` ## MCP `/v1/mcp` never deploys: publishing a version stays with you. In your editor, `vestra mcp` offers a `deploy` tool that runs on your own computer, with the key `vestra login` saved. ## Good to know ### Your first deploy A deployment holds your agent's instructions, the apps and tools it may use, the actions that need the user's approval, and its limits. `deploy` always sends your whole file. The first time, it creates the deployment. A file with no changes makes no new version. A deployment created with `POST /v1/deployments` starts with its draft as version 1, so its first saved version is 2. ### How deploy waits for your tests `vestra deploy` and `vestra deploy --live` wait for the new version's tests, then promote it. Promoting a version makes it the one your agent runs in that mode, test or live. The command prints each result as it arrives (`failed: : `), and then `promoted in test mode, version 5.` The exit code tells CI and coding agents what happened. 0 means the version now runs in that mode, or there was nothing to wait for (an unchanged file, or `--no-promote`). 1 means a test failed, the run ended in an error or was interrupted, or the promotion was refused or another promotion got there first. 2 means the command was written wrong. Ctrl-C stops the waiting (exit 130), but not the test run. If the run passes, it still promotes the version. `--no-wait` returns at once with the run's id, which you can follow with `vestra events `. If you retry a deploy whose version is already being tested, it follows that run. A version that `vestra test` already passed is promoted without testing it again. The `deploy` tool in `vestra mcp` only promotes in test mode, and follows the tests for up to 45 seconds. If the run is still going, it returns the run's id, which you can read with its `test` tool. ### Your default deployment The first deployment you promote becomes your company's default, and this happens only once. If the promotion waits for tests, the deployment becomes the default when they pass. If you deploy the file `vestra init` saved without changing it, it becomes the default at once. From then on, every ask that names no deployment goes to the default. If you set or clear the default yourself, Vestra leaves it as you set it. If an ask goes to the default before it has a version in your key's mode, plain Bash answers it, without your setup. If you name a deployment that has no such version, the ask fails with `deployment_not_live`. Archiving the default deployment clears the default when it has no live version. When it has one, archiving it is refused with `deployment_is_default`. ### Connecting MCP servers Your file registers each MCP server by its URL, and one URL is one server. Its headers are shared by every deployment and mode that uses it. So if another deployment runs the server and your file would give it different headers, the deploy fails with `409 mcp_server_shared`, naming those deployments. The tool policy is not shared. The `deny`, `require_approval` and `run_without_asking` lists in your file belong to the version the file makes, stored as `manifest.mcp_tool_policy`. They take effect when that version is promoted. The policy set on the server itself (`PATCH /v1/mcp-servers/{id}`) is a minimum that every version runs under, and a version's own policy can only add to it. A tool that the server does not mark read-only (`readOnlyHint`) needs the person's approval in every version, unless that version's `run_without_asking` lists it. Bash trusts your server's read-only mark, so mark only tools that change nothing. If your file's `run_without_asking` names a tool your server does not list, the deploy fails with `400 tool_not_on_server`. A test key cannot change or delete a server that a live version uses. That fails with `409 mcp_server_live`, so use a live key, or make the change as an owner or admin. Every version reaches every company server as it is, so while a deployment runs a version live, a test key (or a developer in the console) cannot add, change or delete any server. A file that would register a new one fails with `409 shared_change` before anything is written. ### Skills and knowledge Skills and knowledge are not versioned yet: every version, the live one included, reads them as they are now. So while a deployment runs a version live, a test key cannot change them. A test key's write to a skill or a knowledge notebook, including the uploads `vestra deploy` makes, fails with `409 shared_change` before anything is written, and names the live deployments. `vestra deploy` then exits with code 2 and names what your file changes. Apply those changes with a live key (`vestra deploy --live`), which changes them for your users at once, or as an owner or admin. Until a version is live, a test key can change them freely. ### Functions that need approval Before Bash calls a function your code runs, it asks the person for approval. Two marks in your file skip that: `read_only: true` for a function that changes nothing, such as a lookup, and `run_without_asking: true` for one that changes something but needs no approval. Either mark lets the function run at once, and you can use both together. Bash trusts the marks in your file, so mark only functions that do what the mark says. The approval shows every input of the call. So a function that charges, books or changes something should take each value the person agrees to as an input. For example, `extend_rental` takes `rental_id`, `charge_usd` and `new_due_back`, not `rental_id` alone. Your code then checks those values against its own before it acts, and throws if one is different. The throw becomes the call's error, and nothing is charged. In a conversation, Bash starts a separate task to call a function that needs approval, and that task asks first. A version saved before 3 October 2026 has no marks, so every one of its functions needs approval: mark your lookups `read_only: true`. `vestra init` marks lookups for you. A routine, a batch, or an ask with no person in your app has nobody to approve, so a function it calls waits until the approval expires. Mark a function that may run there `run_without_asking: true`. ### Going live and rolling back A version goes live only after its tests pass. Rolling back needs a live key, and puts the version that was live before back in service at once, without running tests. Your webhooks receive `deployment.version_created`, `deployment.promoted` and `deployment.rolled_back`. ### The key you need Deploying and the other setup commands need a key with the `setup` scope, or `all`. ## The routes behind deploy - `GET` [/v1/mcp-servers](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers.md): Your registered MCP servers. Key scope: `setup` (or `all`). - `POST` [/v1/mcp-servers](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers.md): Register one: URL, auth (none, or headers using ${secret:NAME} or ${user_secret:NAME}), and its tool policy floor: tools never run and tools that always need approval, for every deployment in both modes. A deployment version's own tool policy adds to the floor and never removes from it. Key scope: `setup` (or `all`). - `GET` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id.md): One server and its settings. Key scope: `setup` (or `all`). - `PATCH` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/patch-v1-mcp-servers-server-id.md): Change URL, auth or the tool policy floor, for every deployment in both modes. Users pick it up at their next task. While a deployment's live version uses the server, a test key's change answers 409 mcp_server_live: change it with a live key, or as an owner or admin. Key scope: `setup` (or `all`). - `DELETE` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-mcp-servers-server-id.md): Remove it from every user. While a deployment's live version uses it, a test key answers 409 mcp_server_live. Key scope: `setup` (or `all`). - `POST` [/v1/mcp-servers/{server_id}/check](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers-server-id-check.md): Connect now and list its tools, to prove it works. Key scope: `setup` (or `all`). - `GET` [/v1/mcp-servers/{server_id}/tools](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id-tools.md): The tools we found, each with its setting in the server's tool policy floor. A deployment version's own policy can add to it. Key scope: `setup` (or `all`). - `GET` [/v1/skills](https://vestra.ai/developers/reference/api/skills/get-v1-skills.md): Your company skills, plus the skills Bash already has. A company skill carries sha256, the hash of its files as uploaded, computed the way the SDKs compute a folder's, so an unchanged folder is never uploaded again. Key scope: `setup` (or `all`). - `POST` [/v1/skills](https://vestra.ai/developers/reference/api/skills/post-v1-skills.md): Upload a skill folder as a zip, or as JSON files. Key scope: `setup` (or `all`). - `GET` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id.md): One skill and its files. Key scope: `setup` (or `all`). - `PUT` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/put-v1-skills-skill-id.md): Upload a new version. Key scope: `setup` (or `all`). - `DELETE` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/delete-v1-skills-skill-id.md): Remove it from every user. Key scope: `setup` (or `all`). - `GET` [/v1/skills/{skill_id}/versions](https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id-versions.md): Every version, each with a content hash. Key scope: `setup` (or `all`). - `POST` [/v1/notebooks](https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks.md): Create a notebook. Key scope: `setup` (or `all`). - `GET` [/v1/notebooks](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks.md): Your notebooks. Key scope: `setup` (or `all`). - `GET` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id.md): One notebook and who may read it. Key scope: `setup` (or `all`). - `PATCH` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/patch-v1-notebooks-notebook-id.md): Rename it, or set who may read it: every user, or named users only. Key scope: `setup` (or `all`). - `DELETE` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id.md): Delete it. Key scope: `setup` (or `all`). - `POST` [/v1/notebooks/{notebook_id}/documents](https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks-notebook-id-documents.md): Add a document, as a file or as text. Key scope: `setup` (or `all`). - `GET` [/v1/notebooks/{notebook_id}/documents](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id-documents.md): Its documents. Key scope: `setup` (or `all`). - `DELETE` [/v1/notebooks/{notebook_id}/documents/{document_id}](https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id-documents-document-id.md): Remove one. Key scope: `setup` (or `all`). - `GET` [/v1/deployments](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments.md): Your deployments. Key scope: `setup` (or `all`). - `POST` [/v1/deployments](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments.md): Create one on base 'bash'. It starts with a draft version. Key scope: `setup` (or `all`). - `GET` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id.md): One deployment, by its id or its name: the live version and the draft. With format=yaml, its latest version as vestra.yaml. Key scope: `setup` (or `all`). - `PUT` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/put-v1-deployments-deployment-id.md): Apply a whole vestra.yaml, sent as JSON, by the deployment's name: the first call makes it, and a file that changes nothing makes no version (changed: []). MCP servers are registered by URL, and the tool policy the file gives each one is this version's own, on top of the server's floor, from the moment the version runs. Tests become scenarios, and the routines and webhooks the file declares are its own. ?promote=after_tests (the default), now or no promotes in the key's mode. 400 secret_missing names the secrets to store first; 409 mcp_server_shared names the other deployments that run an MCP server whose headers the file would change. Key scope: `setup` (or `all`). - `PATCH` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/patch-v1-deployments-deployment-id.md): Rename, describe or archive it. Key scope: `setup` (or `all`). - `POST` [/v1/deployments/{deployment_id}/versions](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions.md): Save a new version: instructions, language, MCP servers and this version's own tool policy for them (mcp_tool_policy, on top of each server's floor), allowed apps, approval rules, model, memory mode, notebooks, limits per task. Key scope: `setup` (or `all`). - `GET` [/v1/deployments/{deployment_id}/versions](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions.md): Every version. Key scope: `setup` (or `all`). - `GET` [/v1/deployments/{deployment_id}/versions/{version}](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions-version.md): One version, in full. Key scope: `setup` (or `all`). - `GET` [/v1/deployments/{deployment_id}/diff](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-diff.md): What changed between two versions (?from=&to=). Key scope: `setup` (or `all`). - `POST` [/v1/deployments/{deployment_id}/versions/{version}/promote](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions-version-promote.md): Make it live. Refused unless its latest test run passed. Key scope: `setup` (or `all`). - `POST` [/v1/deployments/{deployment_id}/rollback](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-rollback.md): Put an earlier version back live, instantly. Key scope: `setup` (or `all`). Next: [`test`](https://vestra.ai/developers/test.md) --- url: https://vestra.ai/developers/test.md --- # test > Run your test scenarios against a version before real users see it. Run your test scenarios against a version of your agent before real users see it. A scenario is a pretend user with a goal, a description of success, and things that must never happen. A test run plays every scenario, and an AI judge grades each conversation. You see what failed, and why, before a real user does. ## TypeScript ```ts // Run the tests. Each test in vestra.yaml is a scenario. This runs them all and waits for the judge. const { passed, failed, outcome } = await vestra.test('vestra.yaml'); // outcome is passed or failed, or says more about how it went, such as: passed, interrupted by a restart and resumed. console.log(`${passed} passed, ${failed} failed: ${outcome}`); // Draft more tests. Bash writes new ones for you to add to vestra.yaml, each with what your tools return in it // (tool_results). for (const drafted of await vestra.test.generate('vestra.yaml', { count: 3 })) console.log(drafted.name, drafted.tool_results); ``` ## Python ```python # Run the tests. Each test in vestra.yaml is a scenario. This runs them all and waits for the judge. result = vestra.test("vestra.yaml") # outcome is passed or failed, or says more about how it went, such as: passed, interrupted by a restart and resumed. print(f"{result.passed} passed, {result.failed} failed: {result.outcome}") # Draft more tests. Bash writes new ones for you to add to vestra.yaml, each with what your tools return in it # (tool_results). for drafted in vestra.test.generate("vestra.yaml", count=3): print(drafted.name, drafted.get("tool_results")) ``` ## CLI ```bash # Run the tests. The command waits for the judge, and a failed test makes it fail, so your CI fails too. # It prints how the run went: passed, failed, or more, such as passed, interrupted by a restart and resumed. vestra test # Print the run as JSON. Use this in a script. vestra test --json # Draft more tests. Bash writes them into vestra.yaml, each with what your tools return in it. vestra test --generate ``` ## HTTP ```bash # Draft more tests. The tests in your file are the deployment's scenarios, and Bash can draft more from your # instructions. curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' # Run the tests. This plays every scenario against the latest version, by the deployment's name. Then read # each verdict. If a restart cuts a run off, it resumes on its own: resumes says how often, and error_code says # why a run ended in error. curl -X POST "$VESTRA_BASE_URL/v1/deployments/trip-rescue/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_2k1" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "test", "arguments": { "deployment": "trip-rescue" } } } ``` ## Good to know ### Where tests come from Each time your file is applied (saved to Vestra by `deploy`, or by `test` after an edit), the `tests` in `vestra.yaml` become the deployment's scenarios. A new version is promoted only when they pass. `vestra test --generate` drafts more tests from your instructions. It drops a draft that reuses a test's name or copies its goal word for word, and it is told to avoid repeating the rest. The `tool_results` it writes copy the field names and formats of each tool's example, or of your existing tests. It reads the tests as your file was last applied, so after an edit, run `vestra test` first. ### Testing the file you just edited `vestra test` tests the `vestra.yaml` you just edited, so a test after a fix tests the fix. With a test key, if the file changed since the deployment's latest version, Vestra first saves it as a new version without promoting it (as `vestra deploy --no-promote` does), and tests that version. It tells you so: `vestra.yaml changed since version 4: testing it as version 5, not promoted.` If the deployment does not exist yet, this creates it. On a pass, it adds `version 5 is not promoted: vestra deploy promotes it without testing it again.` Skills and knowledge are not versioned yet: every version, the live one included, reads them as they are now. So if your edit changes a skill or a knowledge document, `vestra test` writes nothing, names what changed and exits with code 2, so that you apply it with `vestra deploy` (with a live key while a deployment runs live). A live key saves nothing and tests the latest version. An unchanged file tests the latest version, and `vestra test ` or `--version ` tests a version already saved in Vestra. The `test` tool in `vestra mcp` works the same way. ### Your tools in a test A test never runs your code. Instead, `tool_results` gives what each of your tools returns in that test, such as `tool_results: { get_booking: { cabin_kg: 7 } }`. It can only name tools your file declares. Any other name is refused with `tool_not_declared`. A tool with no entry returns an error for every call. The transcript records each call's arguments, and notes that `tool_results` answered it. ### Your MCP server's tools in a test A test never runs an MCP server tool that the server does not mark read-only. Its `tool_results` entry answers instead, and without an entry the tool does not run. The entry's key is `.`, using the server's slug, never its `name`. A server your file registers gets its slug from its URL's host: lowercase letters, digits and dashes, starting with a letter, at most 40 characters. For example, `https://mcp.tripnest.example/mcp` becomes `mcp-tripnest-example`. If another server already has that slug, Vestra adds `-2`, `-3` and so on. A server you register with `POST /v1/mcp-servers` keeps the slug you gave it. `GET /v1/mcp-servers` lists each server's `slug`. When no entry answered a call, the transcript says which key would have. ### Approvals in a test By default, the pretend user says no to every approval. So a test of an action checks that Bash asks first, and if the test fails after a no, its result says so. A test with `approves: true` says yes to each approval for your MCP server's tools and your functions, as your user would in your app. That lets it check what Bash does after the yes. The tool still returns its `tool_results` entry, or does not run. Every other approval, such as a browser action on an account or connecting an app, is declined in every test, and a failure after such a no says so. `vestra test --generate` writes `approves: true` on a test whose user agrees to such a tool's action. It checks what the approval shows, such as `The approval shows the charge of 12 and the new return time 2026-10-07 17:30.`, never what Bash says before it. When one of your functions needs approval, the starter tests include one where the user says yes and one where the user says no. The no test checks that Bash says the action did not happen. ### If a run is interrupted A test run survives a restart of Vestra's servers. If a restart cuts a run off, it resumes where it stopped. It keeps the verdicts it already has and plays its unfinished tests again, and each verdict is billed once. The result says so, as in `passed, interrupted by a restart and resumed`. A resumed run that passes promotes the version as usual. A run cut off a third time (`interrupted 3 times by restarts`), or lost for longer than 15 minutes, ends in `error` with `interrupted` and promotes nothing. Start it again. ### How tests are judged An AI judge grades each conversation against what to `expect` and what must `never` happen. A failure goes to a second, stronger judge. It comes from a different model maker than the models that played the test, so no model grades its own maker's work. If the second judge passes the test, the test passes, and the reason quotes both judges and names the second. If it fails the test too, the failure stands. A pass is judged only once. If no second judge could check a failure, the failure stands, and its reason says why. ### What a run costs Each scenario bills its judge's call along with the work it does. A failing scenario also bills the second judge's call. The run's `cost_usd` is the total. ### When a run finishes Your webhooks receive `test_run.completed` with how many tests passed, and an `error_code` that is null unless the run ended in `error`. The CLI and the SDKs say in plain words how the run ended, never with a bare error. Each result includes its conversation and the reason for its verdict. ## The routes behind test - `GET` [/v1/deployments/{deployment_id}/scenarios](https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-scenarios.md): The deployment's test scenarios. Key scope: `setup` (or `all`). - `POST` [/v1/deployments/{deployment_id}/scenarios](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios.md): Add one: a pretend user, their goal, what success looks like, what must never happen. Key scope: `setup` (or `all`). - `POST` [/v1/deployments/{deployment_id}/scenarios/generate](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios-generate.md): Have Bash draft scenarios from the deployment's instructions. Key scope: `setup` (or `all`). - `PATCH` [/v1/deployments/{deployment_id}/scenarios/{scenario_id}](https://vestra.ai/developers/reference/api/tests/patch-v1-deployments-deployment-id-scenarios-scenario-id.md): Change one. Key scope: `setup` (or `all`). - `DELETE` [/v1/deployments/{deployment_id}/scenarios/{scenario_id}](https://vestra.ai/developers/reference/api/tests/delete-v1-deployments-deployment-id-scenarios-scenario-id.md): Remove one. Key scope: `setup` (or `all`). - `POST` [/v1/deployments/{deployment_id}/test-runs](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-test-runs.md): Run every scenario against a version: version, or the latest when left out. The deployment is named by its id or its name. user_secrets sets the test user's own secrets for your MCP servers, replacing any an earlier run set. Key scope: `setup` (or `all`). - `GET` [/v1/deployments/{deployment_id}/test-runs](https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-test-runs.md): Past test runs. Key scope: `setup` (or `all`). - `GET` [/v1/test-runs/{test_run_id}](https://vestra.ai/developers/reference/api/tests/get-v1-test-runs-test-run-id.md): Pass or fail per scenario, the conversation and the judge's reason. Key scope: `setup` (or `all`). Next: [`ask`](https://vestra.ai/developers/ask.md) --- url: https://vestra.ai/developers/ask.md --- # ask > Send a job in plain words, for one user, many users or your company. Send a job in plain words to Bash, the AI agent Vestra runs for you. The job can be for one of your users, for many users at once, or for your own company. Bash can reply right away, act in apps, call your tools, or start a task (a longer piece of work) on its own computer. You can follow every step as it happens. ## TypeScript ```ts import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // reads VESTRA_API_KEY and VESTRA_BASE_URL // Ask for one of your users. Name them with your own id. Their Bash is created the first time you do. const traveller = vestra.user('traveller-42'); const run = traveller.ask('My 6am flight got cancelled. What should I do?'); for await (const event of run) { if (event.type === 'task.started') console.log(`Working on: ${event.data.title ?? 'a task'}`); } console.log(await run.text()); // Ask a follow-up. It goes to the same conversation. console.log(await run.ask('Can I get a refund for it?').text()); // Stop a reply and its work. This stops the reply and every task the conversation started. const hotel = traveller.ask('Find me a hotel near the airport.'); await hotel.stop(); // Ask for your company. Leave the user out, and the job goes to your company's own Bash. console.log(await vestra.ask('Write a two-line notice for our travellers about storm delays at Mumbai airport today.').text()); // Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, // that item waits for the answer, and so does wait(). const affected = [ { id: 'traveller-42', city: 'Mumbai' }, { id: 'traveller-77', city: 'Pune' }, ]; const batch = vestra.ask(affected.map((t) => ({ user: t.id, message: `A storm may delay flights from ${t.city} today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking.` }))); console.log((await batch.wait()).counts); ``` ## Python ```python from vestra import Vestra vestra = Vestra() # reads VESTRA_API_KEY and VESTRA_BASE_URL # Ask for one of your users. Name them with your own id. Their Bash is created the first time you do. traveller = vestra.user("traveller-42") run = traveller.ask("My 6am flight got cancelled. What should I do?") for event in run: if event.type == "task.started": print(f"Working on: {event.data.get('title') or 'a task'}") print(run.text()) # Ask a follow-up. It goes to the same conversation. print(run.ask("Can I get a refund for it?").text()) # Stop a reply and its work. This stops the reply and every task the conversation started. hotel = traveller.ask("Find me a hotel near the airport.") hotel.stop() # Ask for your company. Leave the user out, and the job goes to your company's own Bash. print(vestra.ask("Write a two-line notice for our travellers about storm delays at Mumbai airport today.").text()) # Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, # that item waits for the answer, and so does wait(). affected = [{"id": "traveller-42", "city": "Mumbai"}, {"id": "traveller-77", "city": "Pune"}] batch = vestra.ask([{"user": t["id"], "message": f"A storm may delay flights from {t['city']} today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking."} for t in affected]) print(batch.wait().counts) ``` ## CLI ```bash # Ask for one of your users. Approvals and questions appear right here in the terminal. Ctrl-C stops it. vestra ask "My 6am flight got cancelled. What should I do?" --user traveller-42 # Ask a follow-up. It goes to the same conversation. vestra ask "Can I get a refund for it?" --user traveller-42 --conversation conv_8f2k # Stop a reply and its work. This stops the reply and every task the conversation started. vestra ask --stop conv_8f2k # Ask for your company. Leave the user out, and the job goes to your company's own Bash. vestra ask "Write a two-line notice for our travellers about storm delays at Mumbai airport today." # Ask for many users at once. Pass a CSV with one user id per line, and each user gets their own conversation. # If Bash asks one of them a question, that item waits for the answer. vestra ask "A storm may delay flights from Mumbai today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." --users travellers.csv ``` ## HTTP ```bash # Ask for one of your users. Their Bash is created the first time you name them. With stream: true, the # response streams this turn's events as they happen, and ends when the turn is over. 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. What should I do?", "stream": true }' # Ask a follow-up. It goes to the same conversation. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Can I get a refund for it?", "stream": true }' # Stop a reply and its work. This stops the reply and every task the conversation started. curl -X POST "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/stop" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Ask for your company. Leave user_id out, and the job goes to your company's own Bash. curl "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Write a two-line notice for our travellers about storm delays at Mumbai airport today." }' # Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, # that item waits for the answer. curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "A storm may delay flights from Mumbai today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." }, { "user_id": "traveller-77", "message": "A storm may delay flights from Pune today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." } ] }' ``` ## MCP ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` ## Good to know ### Who the job is for `vestra.user(id)` acts for one of your users, named by your own id. The id is 1 to 128 characters and starts with a letter or digit. The user is created the first time you name them. Leave the user out, and the job goes to your company's own Bash, the one that works for your company rather than for a user. Over HTTP, the field is `user_id`. ### When the reply has no text A turn is your message and Bash's reply to it. When Bash only starts a task and says nothing, the turn can end with an empty `message.completed.text`. The task's name is in the `task.started` event, in its `data.title`, and its result arrives with `task.completed`. ### When a job is stopped or fails When a job is stopped or fails, the turn ends first, before the events that end its tasks arrive. A `stream: true` call closes when the turn ends. Then `task.stopped` or `task.failed` follow on the conversation's stream, on `GET /v1/events` and on your webhooks. ### Conversation status A conversation is `idle` when nothing is happening, `replying` while Bash writes its reply, `working` while tasks it started are running, and `waiting` while Bash waits for you: an approval, an answer, a hand-off (a step only a person can do) or one of your tools. Each change arrives as a `conversation.status` event, with the turn's `turn_id` and `turn_status`. ### Asking many users at once Pass a list of `{ user, message }` to ask many users at once. This is a batch (`POST /v1/batches`), and each item gets its own conversation. Items with no user run on your company's own Bash, at most 5 at a time. ### What a job cost `run.cost()` returns the conversation's billed cost once the run ends. `cost_complete` stays false while charges from the tasks it started can still arrive. For the itemized receipt, read the line items on [`usage`](https://vestra.ai/developers/usage.md). ## The routes behind ask - `POST` [/v1/conversations](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations.md): Ask. Start a conversation, optionally with its first message and that message's options (context, attachments, output_schema, success_criteria, tools, hand_off). user_id names whose Bash it is, created on first use; leave it out for the company's own Bash. stream: true answers the first turn's events as text/event-stream and ends when the turn settles. Key scope: `runtime` (or `all`). - `GET` [/v1/conversations](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations.md): Conversations, newest first: one user's with user_id, or every one of the company's without it (the company's own Bash shows user_id null). Key scope: `runtime` (or `all`). - `GET` [/v1/conversations/{conversation_id}](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id.md): One conversation: status (idle, replying, working, waiting), its open work, its open approvals. Key scope: `runtime` (or `all`). - `POST` [/v1/conversations/{conversation_id}/messages](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-messages.md): Send a message. Options: context (data for this turn only), attachments, output_schema, success_criteria, tools, hand_off (auto or always). Answers the turn at once; Prefer: wait=N holds for the final answer; stream: true answers this turn's events as text/event-stream and ends when the turn settles. Key scope: `runtime` (or `all`). - `GET` [/v1/conversations/{conversation_id}/messages](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-messages.md): The conversation, paged: your messages, Bash's replies, and summaries of the work it did. Key scope: `runtime` (or `all`). - `POST` [/v1/conversations/{conversation_id}/stop](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-stop.md): Stop the reply and every piece of work the conversation started. Key scope: `runtime` (or `all`). - `GET` [/v1/tasks](https://vestra.ai/developers/reference/api/work/get-v1-tasks.md): All work across your users, or one user's with user_id. Filter by status, deployment, conversation or date too. Key scope: `runtime` (or `all`). - `GET` [/v1/tasks/{task_id}](https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id.md): Status, title, the progress list, the result (text, and data when an output_schema was set), the outcome, cost so far. Key scope: `runtime` (or `all`). - `DELETE` [/v1/tasks/{task_id}](https://vestra.ai/developers/reference/api/work/delete-v1-tasks-task-id.md): Delete it and its outputs. Key scope: `runtime` (or `all`). - `POST` [/v1/tasks/{task_id}/stop](https://vestra.ai/developers/reference/api/work/post-v1-tasks-task-id-stop.md): Stop this piece of work only. Key scope: `runtime` (or `all`). - `POST` [/v1/batches](https://vestra.ai/developers/reference/api/batches/post-v1-batches.md): Start a batch: a deployment, a list of items (user_id optional, message, context, metadata) and a concurrency. Each item is one conversation message with hand_off: always. Key scope: `runtime` (or `all`). - `GET` [/v1/batches/{batch_id}](https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id.md): Progress: counts by status, spend so far. Key scope: `runtime` (or `all`). - `GET` [/v1/batches/{batch_id}/items](https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id-items.md): Each item with its task id and result. Key scope: `runtime` (or `all`). - `POST` [/v1/batches/{batch_id}/cancel](https://vestra.ai/developers/reference/api/batches/post-v1-batches-batch-id-cancel.md): Stop starting new items; running ones finish. Key scope: `runtime` (or `all`). Next: [`routines`](https://vestra.ai/developers/routines.md) --- url: https://vestra.ai/developers/routines.md --- # routines > Run a job on a schedule, or each time your system calls a URL. Run a job on a schedule, or each time your system calls a URL. A routine is a saved job that runs on a schedule written in plain words, such as `weekday at 08:00`, or when your system posts to its trigger URL. Each run appears in the routine's conversation like any other ask, so its replies, approvals and files are where you already look. ## TypeScript ```ts const traveller = vestra.user('traveller-42'); // Run on a schedule. Write it in plain words, read in the traveller's own time zone. The response gives the // cron expression and the next run. const daily = await traveller.routines.create({ ask: 'Check my trips for today and tell me about any delay.', every: 'weekday at 08:00', }); console.log(daily.schedule?.cron, daily.next_run_at); // Run when your system calls. Post a payload to the routine's trigger_url. const onDelay = await traveller.routines.create({ ask: 'A train of mine is late. Find me another way.', on: 'call' }); await traveller.routines.trigger(onDelay.id, { train: '12127', delay_minutes: 45 }); // Manage a routine. Pause it, resume it, run it once now, and read its runs. await traveller.routines.pause(daily.id); await traveller.routines.resume(daily.id); await traveller.routines.run(daily.id); const runs = await traveller.routines.runs(daily.id); console.log(runs.data.length); ``` ## Python ```python traveller = vestra.user("traveller-42") # Run on a schedule. Write it in plain words, read in the traveller's own time zone. The response gives the # cron expression and the next run. daily = traveller.routines.create("Check my trips for today and tell me about any delay.", every="weekday at 08:00") print(daily.schedule.cron, daily.next_run_at) # Run when your system calls. Post a payload to the routine's trigger_url. on_delay = traveller.routines.create("A train of mine is late. Find me another way.", on="call") traveller.routines.trigger(on_delay.id, {"train": "12127", "delay_minutes": 45}) # Manage a routine. Pause it, resume it, run it once now, and read its runs. traveller.routines.pause(daily.id) traveller.routines.resume(daily.id) traveller.routines.run(daily.id) print(len(traveller.routines.runs(daily.id).data)) ``` ## CLI ```bash # Run on a schedule. Write it in plain words, read in the traveller's own time zone. vestra routines add "Check my trips for today and tell me about any delay." \ --every "weekday at 08:00" --user traveller-42 # Run when your system calls. Your system posts to the routine's trigger_url. vestra routines add "A train of mine is late. Find me another way." --on call --user traveller-42 # Manage a routine. List the user's routines, then run one now, pause it, resume it, and read its runs. vestra routines --user traveller-42 vestra routines run rtn_5kd vestra routines pause rtn_5kd vestra routines resume rtn_5kd vestra routines runs rtn_5kd ``` ## HTTP ```bash # Run on a schedule. Write it in plain words, read in the user's own time zone. The response gives the cron # expression and the next run. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Run when your system calls. Post a payload to the routine's trigger_url. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' # Manage a routine. Pause it (enabled: true resumes it), run it once now, and list the user's routines. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' curl -X POST "$VESTRA_BASE_URL/v1/routines/rtn_5kd/run" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/routines?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` ## Good to know ### Writing a schedule `every` understands `day at 09:00`, `weekday at 08:00` (or `weekdays`), `weekend at 10:30`, days such as `mon, wed, fri at 18:30`, `30 minutes` (15, 20 or 30), `2 hours` (1, 2, 3, 4, 6, 8 or 12) and `month on the 1st at 09:00` (the 1st to the 28th). Times use the 24-hour clock. A phrase it cannot read fails with `400 schedule_unreadable`, along with examples that work. You can also pass a cron expression as `schedule`. ### Time zones A schedule is read in the `timezone` you give. Without one, it uses the user's own time zone, and then UTC. The response includes the cron expression and the next run time. ### Routines Bash creates If you ask Bash in a conversation to check your trips every weekday at 8, it creates the routine itself. It appears in the list like any other routine. ### Routine limits Your company can have up to 200 routines in each mode (test or live), paused ones included. One more fails with `409 org_routine_limit_reached`. Running routines yourself with `run` and `trigger` is limited to 12 times an hour for one routine, and 240 times an hour for your company. ### A copy for each user A routine your file declares with `for: each customer` gets one copy per user, made the first time that user talks to that deployment's Bash. Copies count toward the same limit, 200 per mode. A copy past the limit is skipped with `org_routine_limit_reached` and never retried. ### When a routine runs Each run sends a `routine.fired` event that names the run and its conversation, on the stream and to your webhooks. ## The routes behind routines - `POST` [/v1/routines](https://vestra.ai/developers/reference/api/routines/post-v1-routines.md): Create one: what to ask, and when: every (plain words such as "weekday at 08:00", read in the user's time zone), a cron schedule, or on "call" (your system posts to its trigger_url). For a user, or with no user_id for the company's own Bash. Each run is a message in the routine's conversation. Key scope: `runtime` (or `all`). - `GET` [/v1/routines](https://vestra.ai/developers/reference/api/routines/get-v1-routines.md): The routines of one user (user_id), or of the company's own Bash. Key scope: `runtime` (or `all`). - `GET` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id.md): One routine. Key scope: `runtime` (or `all`). - `PATCH` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/patch-v1-routines-routine-id.md): Change what it asks, when it runs (every, schedule or timezone), its filters, or pause and resume it. Key scope: `runtime` (or `all`). - `DELETE` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/delete-v1-routines-routine-id.md): Delete it. Key scope: `runtime` (or `all`). - `POST` [/v1/routines/{routine_id}/run](https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-run.md): Run it now. Key scope: `runtime` (or `all`). - `POST` [/v1/routines/{routine_id}/trigger](https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-trigger.md): Fire it with a payload from your system: a train delayed, a refund posted. Key scope: `runtime` (or `all`). - `GET` [/v1/routines/{routine_id}/runs](https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id-runs.md): Past runs and the tasks they started. Key scope: `runtime` (or `all`). Next: [`answer`](https://vestra.ai/developers/answer.md) --- url: https://vestra.ai/developers/answer.md --- # answer > Reply when your agent needs an approval, an answer or your tool's result. Reply when Bash needs something from a person or from your code. Each request is an interaction: an approval, a question, a request to connect an app, a hand-off (a step only a person can do), or a call to one of your tools. One route answers every kind. You send the body that fits its type. ## TypeScript ```ts const run = vestra.user('traveller-42').ask('Rebook my cancelled flight.'); // Answer during a run. When Bash needs something, the run sends an interaction.created event. for await (const event of run) { if (event.type !== 'interaction.created') continue; const asked = event.interaction; // what Bash waits on, read for you (null for a call to your own tool) if (asked?.type === 'approval') { // A payment goes through only when the person who saw the approval says yes. await event.answer({ approve: await app.confirm(event.user, asked.summary), present_user: true }); } if (asked?.type === 'question' && asked.questions) { const answers: string[] = []; for (const q of asked.questions) answers.push(await app.choose(event.user, q.question, q.options.map((o) => o.label))); await event.answer({ answers }); } } // Answer later, by id. List everything Bash is waiting on, from anywhere, and answer each one by its id. const waiting = await vestra.answer.list({ user: 'traveller-42' }); for (const item of waiting.data) { if (item.type === 'handoff') await vestra.answer(item.id, { done: true }); } ``` ## Python ```python run = vestra.user("traveller-42").ask("Rebook my cancelled flight.") # Answer during a run. When Bash needs something, the run sends an interaction.created event. for event in run: if event.type != "interaction.created": continue asked = event.interaction # what Bash waits on, read for you (None for a call to your own tool) if asked and asked.type == "approval": # A payment goes through only when the person who saw the approval says yes. event.answer(approve=app.confirm(event.user, asked.summary), present_user=True) if asked and asked.type == "question": event.answer(answers=[app.choose(event.user, q.question, [o.label for o in q.options]) for q in asked.questions]) # Answer later, by id. List everything Bash is waiting on, from anywhere, and answer each one by its id. for item in vestra.answer.list(user="traveller-42").data: if item.type == "handoff": vestra.answer(item.id, done=True) ``` ## CLI ```bash # List what Bash is waiting on. Leave out --user to see what it waits on for every user. vestra answer --user traveller-42 # Approve or deny. --present-user says the person who saw the approval is the one saying yes. To say no, give # a reason. vestra answer int_3j9 --approve --present-user vestra answer int_7kq --deny --message "Not that hotel, it is too far from the airport." # Answer a question. vestra answer int_2mx --answer "Window" # Finish a hand-off. A hand-off is a step only a person can do. vestra answer int_9hd --done # Return your tool's result. vestra answer call_4mz --output '{ "status": "on time", "gate": "B4" }' ``` ## HTTP ```bash # List what Bash is waiting on. For one user, this lists approvals, questions, hand-offs, apps to connect and # calls to your tools. curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Approve or deny. A payment or hands-on approval needs present_user: true, and a hands-on approval also needs # its nonce. To say no, send approve: false. 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 }' # Answer a question. Send one answer per question. curl "$VESTRA_BASE_URL/v1/interactions/int_7qp/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "answers": ["The 09:15 flight"] }' # Return your tool's result. Answer a call to one of your tools with its output, or an error. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "status": "confirmed", "pnr": "100042" } }' ``` ## MCP ```json { "method": "tools/call", "params": { "name": "answer", "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true } } } ``` ## Good to know ### What to send for each type An approval takes `{ approve, present_user?, message?, nonce? }`. A question takes `{ answers }`, with one answer per question, or `{ approve: false }` to skip it. A connect request takes `{ approve: false }` to decline it. A hand-off takes `{ done }`, `{ could_not }`, `{ not_now }` or `{ answers }`. A call to your tool takes `{ output }` or `{ error }`. Any other body fails with `400 answer_mismatch`, and the error names the shapes that fit. ### Approvals that need the person present A payment approval, or a hands-on approval (Bash operating its computer for the user), is accepted only with `present_user: true`, from the person who saw it. The same goes for approving a company action: a tool on one of your MCP servers, or one of your functions that is not marked read-only or run without asking. A company action's `summary` says what will happen, taken from the tool's description, then names the tool and each argument. For example: `Extends a rental by one day. extend_rental: rental_id R-1042, charge_usd 15, new_due_back 2026-10-05 18:00`. The summary is at most 120 characters and never cuts an argument in half. When space runs out, it drops the description first, then each argument that does not fit, and ends with `and N more`. `details.rows` holds the call's first 8 arguments, each value up to 200 characters. Its `moves_money` is `null`, because only your code knows what your tools cost. A tool that takes its price as an input shows the price in the rows. On a hands-on approval, the SDK sends the interaction's `nonce` for you. ### Let the SDK run your tools Pass `new Vestra({ tools })` one function per tool, and the SDK answers each call with what the function returns. If the function throws, that becomes the call's error. This works both on a run and in the webhook handler. ### Running several processes Each process runs your function once per call. If you run several processes, answer your tools through one path only: either your runs or a single webhook endpoint. Otherwise two processes that receive the same call at the same moment can both run it. ### When a request expires A call to your tool that nobody answers expires after its `timeout_s`, and sends `interaction.expired`. For now, only tool calls send expiry events. Other interactions end with `interaction.resolved`. ## The routes behind answer - `GET` [/v1/interactions](https://vestra.ai/developers/reference/api/interactions/get-v1-interactions.md): Everything Bash waits on, across your users: approvals, questions, app connections, hand-offs, and calls to your client tools (type tool_call). Filter by user_id, conversation_id, task_id, type or status (pending by default). Key scope: `runtime` (or `all`). - `GET` [/v1/interactions/{interaction_id}](https://vestra.ai/developers/reference/api/interactions/get-v1-interactions-interaction-id.md): One item, by its int_ or call_ id: its type (approval, question, connect, handoff, tool_call), what Bash wants to do, the options, when it expires. A handoff carries a snapshot of the page. Key scope: `runtime` (or `all`). - `POST` [/v1/interactions/{interaction_id}/answer](https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-answer.md): Answer it, whatever it is. An approval takes approve (true or false), a question answers or approve: false, a hand-off done, could_not or not_now (or answers, when it asks for a value such as a code), a client tool call output or error. A money or account approval, and the yes to a company action (a tool of your MCP servers, or a function of yours that is not read-only or run without asking), must carry present_user: true. A body that fits another kind is 400 answer_mismatch. Key scope: `runtime` (or `all`). - `POST` [/v1/interactions/{interaction_id}/cancel](https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-cancel.md): Withdraw it. The task carries on without that action. Key scope: `runtime` (or `all`). Next: [`watch`](https://vestra.ai/developers/watch.md) --- url: https://vestra.ai/developers/watch.md --- # watch > Show your agent's screen live, and let a person take control. Show a user's Bash at work on its own computer, live. Open a session to get a link to that computer's screen, for a browser tab or an iframe. With control turned on, a person can take over the mouse and keyboard, then hand control back to Bash. ## TypeScript ```ts const traveller = vestra.user('traveller-42'); // Watch the screen live. Open session.url in a browser tab or an iframe. const session = await traveller.watch(); console.log(session.url, session.expires_at); // Let a person take control. They can take over from Bash, hand control back, and end the session. const wheel = await traveller.watch({ control: true }); await wheel.take(); await wheel.giveBack(); await wheel.end(); // Take a screenshot. You get the latest frame of their browser as a JPEG. const jpeg = await traveller.screenshot(); console.log(jpeg.byteLength); ``` ## Python ```python traveller = vestra.user("traveller-42") # Watch the screen live. Open session.url in a browser tab or an iframe. session = traveller.watch() print(session.url, session.expires_at) # Let a person take control. They can take over from Bash, hand control back, and end the session. wheel = traveller.watch(control=True) wheel.take() wheel.give_back() wheel.end() # Take a screenshot. You get the latest frame of their browser as a JPEG. print(len(traveller.screenshot())) ``` ## CLI ```bash # Watch the screen live. You get a link to their screen, valid for 15 minutes. vestra watch --user traveller-42 # Let a person take control. With --control, whoever opens the link can take over from Bash. vestra watch --user traveller-42 --control ``` ## HTTP ```bash # Watch the screen live. Create a session with a screen to watch, and open its url in a browser tab or an iframe. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "screen": "view" }' # Let a person take control. In a session opened with "screen": "control", the person can take over from Bash, # using the session's own token. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/screen" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "take" }' # Take a screenshot. You get the latest frame of the user's browser as a JPEG. curl "$VESTRA_BASE_URL/v1/users/traveller-42/screenshot" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o screen.jpg ``` ## MCP ```json { "method": "tools/call", "params": { "name": "watch", "arguments": { "user": "traveller-42", "control": true } } } ``` ## Good to know ### What a session is `watch`, `talk` and [`embed`](https://vestra.ai/developers/embed.md) all open a session (`POST /v1/sessions`): a short-lived link for one user. A session's `url` carries its own token, valid for 15 minutes. The token can only reach that user and the session's conversation, and it never shows what anything costs. ### Who has control The `screen.control` event says when the person takes control and when Bash has it back. Each call to `GET /v1/sessions/{id}/screen` makes new credentials for the screen's stream, and each set lasts 5 minutes. ### Screenshots `GET /v1/users/{id}/screenshot` returns the latest frame of the user's browser as a JPEG. ## The routes behind watch - `GET` [/v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-screen.md): With the session's token: the stream credentials of the user's Bash browser, good for 5 minutes. For a session opened with a screen. 409 payment_in_progress while the person pays for something in their browser. - `POST` [/v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-screen.md): With the session's token, in control mode: the user takes the wheel or gives it back (action), or says they are driving (input), so Bash waits. - `GET` [/v1/users/{user_id}/screenshot](https://vestra.ai/developers/reference/api/sessions/get-v1-users-user-id-screenshot.md): The latest frame of the user's browser, as a JPEG. 409 browser_in_use while a person finishes a sign-in on it (a browser fetch with front open): its page is for them alone. 409 payment_in_progress while the person pays for something in their browser, or just after. Key scope: `runtime` (or `all`). - `GET` [/v1/users/{user_id}/browser/watch](https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-watch.md): Watch the browser fetch's tabs live, as text/event-stream. Read only: allowed sites only, a tab on a blocked path is hidden, and it never wakes the computer. Key scope: `runtime` (or `all`). Next: [`talk`](https://vestra.ai/developers/talk.md) --- url: https://vestra.ai/developers/talk.md --- # talk > Start a live voice call between a user and your agent. Start a live voice call between one of your users and Bash. Create a session (a short-lived link for that user), open its url, and the page joins the call. Everything said in the call is saved to the same conversation as text. ## TypeScript ```ts // Ask something first. The call below joins this conversation. const traveller = vestra.user('traveller-42'); const run = traveller.ask('Find me a flight to Goa on Friday.'); await run.text(); // Call in an existing conversation. Open call.url, and the page joins the call. const call = await traveller.talk({ conversation: run.conversation! }); console.log(call.url); // Start a new conversation by voice. const fresh = await traveller.talk(); await fresh.end(); ``` ## Python ```python # Ask something first. The call below joins this conversation. traveller = vestra.user("traveller-42") run = traveller.ask("Find me a flight to Goa on Friday.") run.text() # Call in an existing conversation. Open call.url, and the page joins the call. call = traveller.talk(conversation=run.conversation) print(call.url) # Start a new conversation by voice. fresh = traveller.talk() fresh.end() ``` ## CLI ```bash # Call in an existing conversation. You get a link, and what is said is saved to that conversation. vestra talk --user traveller-42 --conversation conv_8f2k ``` ## HTTP ```bash # Call in an existing conversation. Open the session's url, and the page joins the call. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "voice": true }' # Connect your own voice client. The session's page does this for you: it sends the browser's WebRTC offer # with the session's own token, and gets the answer back. Call it yourself only if you build your own client. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/voice" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sdp": "the WebRTC offer from the browser" }' ``` ## MCP ```json { "method": "tools/call", "params": { "name": "talk", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` ## Good to know ### When the call ends The `session.ended` event carries the call's length in `seconds`, and its `transcript`. ### Which setup answers the call Bash answers with the conversation's deployment, which is your agent's setup. If the conversation has none, it uses the user's default deployment, and then your company's. ### Building your own voice client The session's page sends the browser's WebRTC offer to `POST /v1/sessions/{id}/voice` with the session's token. Call this route yourself only if you build your own voice client. Vestra accepts calls only from its own page and from your allowed origins. ## The routes behind talk - `POST` [/v1/sessions/{session_id}/voice](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-voice.md): Called from the user's browser with the session's token and its WebRTC offer; answers the model's answer. Audio then flows straight to the voice model, and everything said lands in the session's conversation. For a session opened with voice. Next: [`events`](https://vestra.ai/developers/events.md) --- url: https://vestra.ai/developers/events.md --- # events > Follow each step your agent takes, live or afterwards, or as webhooks. Follow what Bash is doing. Every step it takes is an event: the reply as it is written, tasks starting and finishing, approvals, files and costs. You can stream one conversation's events live, read every step of a task afterwards, or have events sent to your server as webhooks. ## TypeScript ```ts // Stream a conversation live. You get its stored events first, then new ones as they happen. for await (const event of vestra.events('conv_8f2k')) { console.log(event.type, event.data); if (event.type === 'conversation.status' && event.data.turn_status === 'completed') break; } // Read a task afterwards. You get every step of one task, with tokens and cost per turn. const trace = await vestra.events.trace('task_91d'); // Send events to your server. Webhooks are signed and have the same shape. The signing secret is shown only once. const endpoint = await vestra.events.to('https://hooks.example.com/vestra', { events: ['task.completed', 'interaction.created', 'usage.cap_reached'] }); console.log(endpoint.secret); app.post('/vestra', express.raw({ type: 'application/json' }), vestra.events.handler(async (event) => { if (event.type === 'task.completed') await markDone(event.task_id, event.data.cost_usd); })); // Catch up on what you missed. List the events your webhooks were sent, newest first. const failed = await vestra.events.list({ type: 'task.failed' }); console.log(trace, failed.data.length); ``` ## Python ```python # Stream a conversation live. You get its stored events first, then new ones as they happen. for event in vestra.events("conv_8f2k"): print(event.type, event.data) if event.type == "conversation.status" and event.data.turn_status == "completed": break # Read a task afterwards. You get every step of one task, with tokens and cost per turn. trace = vestra.events.trace("task_91d") # Send events to your server. Webhooks are signed and have the same shape. The signing secret is shown only once. endpoint = vestra.events.to("https://hooks.example.com/vestra", events=["task.completed", "interaction.created", "usage.cap_reached"]) print(endpoint.secret) def on_event(event): if event.type == "task.completed": mark_done(event.task_id, event.data.get("cost_usd")) handler = vestra.events.handler(on_event) # reads VESTRA_WEBHOOK_SECRET @app.post("/vestra") def vestra_webhook(): # Flask: hand it the raw body, as it came status, body = handler(request.get_data(), request.headers) return body, status # Catch up on what you missed. List the events your webhooks were sent, newest first. failed = vestra.events.list(type="task.failed") print(trace, len(failed.data)) ``` ## CLI ```bash # Catch up on what you missed. List the events your webhooks were sent, newest first. vestra events --type task.completed --user traveller-42 # Read a task afterwards. You get each step, and what it cost. vestra events task_91d # Send events to your server. They arrive signed, and the signing secret is shown only once. vestra events --to https://hooks.example.com/vestra --type task.completed,interaction.created # vestra events conv_8f2k follows a conversation as it happens, and # vestra events --to localhost:3000/vestra forwards events to this computer while you build. # Both run until you press Ctrl-C. ``` ## HTTP ```bash # Stream a conversation live. If the connection drops, 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: 41" # Read the same events as JSON. You get them in pages, after the seq you give. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events?format=json&after=41" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Read a task afterwards. You get every step of one task, with tokens and cost per turn. curl "$VESTRA_BASE_URL/v1/tasks/task_91d/trace" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Send events to your server. Register a URL and the events it wants. The signing secret is shown only once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created", "usage.cap_reached"] }' # Catch up on what you missed. List the events your webhooks were sent, newest first. curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } } } ``` ## Good to know ### The event shape in the SDKs However an event arrives, the SDK gives you the same shape: `{ id, seq, type, created_at, user, conversation_id, task_id, data, via }`. `user` is your id for the user, or null for your company's own Bash. Once you check `event.type`, `event.data` has the matching type. ### Streaming over HTTP `GET /v1/conversations/{id}/events` streams Server-Sent Events. Each frame has `id: `, `event: ` and `data: { seq, type, data, created_at }`. Reconnect with `Last-Event-ID: `, and you miss nothing. `?format=json&after=` returns the same events as pages of JSON. ### Webhook bodies and signatures A webhook's body is `{ id, object, type, mode, created_at, user_id, task_id, conversation_id, data }`. It is signed in the `Vestra-Signature: t=,v1=` header. The signature is an HMAC-SHA256 of `.` with your endpoint's secret, and it is valid for 300 seconds. `vestra.events.handler` checks it for you. Without the SDK, check it yourself before you trust a body. ### Stream or webhooks The stream carries everything that happens in a conversation. Webhooks carry the events that still matter afterwards: replies, tasks, approvals, files, sessions, routines, usage alerts, and changes to users, connections, deployments, tests and keys. `message.delta`, `task.progress`, `usage.updated` and `screen.control` only arrive on the stream. ### Catching up `GET /v1/events` lists the events your webhooks were sent, newest first. `?after=evt_…` reads forward from an event you saw. ## The routes behind events - `GET` [/v1/conversations/{conversation_id}/events](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-events.md): The one stream (SSE): replies as they are typed, work started and finished, progress, approvals, tool calls, files. Resume with Last-Event-ID. Filter to one task's events with task_id. Key scope: `runtime` (or `all`). - `GET` [/v1/tasks/{task_id}/trace](https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id-trace.md): Every step: tools called with a summary of inputs and outputs, timings, tokens and cost per turn. Key scope: `runtime` (or `all`). - `POST` [/v1/webhooks](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks.md): Register a URL and the events it wants. The signing secret is shown once. Key scope: `setup` (or `all`). - `GET` [/v1/webhooks](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks.md): Your endpoints. Key scope: `setup` (or `all`). - `GET` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id.md): One endpoint. Key scope: `setup` (or `all`). - `PATCH` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/patch-v1-webhooks-endpoint-id.md): Change URL or events, switch it on or off. Key scope: `setup` (or `all`). - `DELETE` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/delete-v1-webhooks-endpoint-id.md): Remove it. Key scope: `setup` (or `all`). - `POST` [/v1/webhooks/{endpoint_id}/roll-secret](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-roll-secret.md): New signing secret; the old one is accepted for 24 hours. Key scope: `setup` (or `all`). - `POST` [/v1/webhooks/{endpoint_id}/test](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-test.md): Send a sample event now. Key scope: `setup` (or `all`). - `GET` [/v1/webhooks/{endpoint_id}/deliveries](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id-deliveries.md): Every attempt with status code and timing. Key scope: `setup` (or `all`). - `POST` [/v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-deliveries-delivery-id-retry.md): Send one again. Key scope: `setup` (or `all`). - `GET` [/v1/events](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events.md): The same events as a list you can pull, for anything you missed. Key scope: `runtime` (or `all`). - `GET` [/v1/events/{event_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events-event-id.md): One event. Key scope: `runtime` (or `all`). Next: [`files`](https://vestra.ai/developers/files.md) --- url: https://vestra.ai/developers/files.md --- # files > Upload files for your agent, and download the files it makes. Upload files for Bash to use, and download the files it makes. Every file it makes records its task and its conversation, so one call lists everything a conversation produced. ## TypeScript ```ts const traveller = vestra.user('traveller-42'); // Upload a file. Pass a path, a Blob or bytes, up to 10 MB. Then ask about it. const ticket = await traveller.files.upload('./ticket.pdf'); const run = traveller.ask('Is this ticket refundable?', { attachments: [ticket.id] }); await run.text(); // Download what Bash made. List one conversation's files, or one task's, and download each one. const made = await vestra.files.list({ conversation: run.conversation! }); for (const file of made.data) { const bytes = await vestra.files.download(file.id); console.log(file.filename, bytes.byteLength); } ``` ## Python ```python traveller = vestra.user("traveller-42") # Upload a file. Pass a path, an open file or bytes, up to 10 MB. Then ask about it. ticket = traveller.files.upload("ticket.pdf") run = traveller.ask("Is this ticket refundable?", attachments=[ticket.id]) run.text() # Download what Bash made. List one conversation's files, or one task's, and download each one. for file in vestra.files.list(conversation=run.conversation).data: print(file.filename, len(vestra.files.download(file.id))) ``` ## CLI ```bash # Upload a file. It can be up to 10 MB. vestra files add ticket.pdf --user traveller-42 # Download what Bash made. List one conversation's files, and download one into this folder. vestra files conv_8f2k vestra files get file_2kq ``` ## HTTP ```bash # Upload a file. Send it for one user as multipart form data, with one part named file, up to 10 MB. curl "$VESTRA_BASE_URL/v1/files?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -F "file=@ticket.pdf" # Download what Bash made. List one conversation's files (its tasks' outputs), or one task's. curl "$VESTRA_BASE_URL/v1/files?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get one file. The response has a download link that works for a short while. curl "$VESTRA_BASE_URL/v1/files/file_2kq" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "files", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` ## Good to know ### Outputs and uploads A file's `source` is `output` if Bash made it, and `upload` if you uploaded it. Listing by `task` or `conversation` returns outputs. Listing by user returns uploads, unless you pass `source`. ### Uploading `POST /v1/files` takes multipart form data with one part named `file`, up to 10 MB. To use the file in an ask, pass its id as an attachment. ### Download links `GET /v1/files/{id}` returns a download link that works for a short while. To get a fresh link, read the file again. ### When Bash makes a file The `file.created` event names the new file, on the stream and on your webhooks. ## The routes behind files - `POST` [/v1/files](https://vestra.ai/developers/reference/api/files/post-v1-files.md): Upload a file for Bash to use: multipart/form-data with one part named file. user_id in the query names whose Bash it is for; without it, the company's own. Key scope: `runtime` (or `all`). - `GET` [/v1/files](https://vestra.ai/developers/reference/api/files/get-v1-files.md): Files, each with its task and conversation: one user's (user_id) or the company's own Bash's, one task's outputs (task_id), or one conversation's, its tasks' outputs (conversation_id). source picks what you handed Bash (upload) or what Bash made (output). Key scope: `runtime` (or `all`). - `GET` [/v1/files/{file_id}](https://vestra.ai/developers/reference/api/files/get-v1-files-file-id.md): Details and a short-lived download link. Key scope: `runtime` (or `all`). - `DELETE` [/v1/files/{file_id}](https://vestra.ai/developers/reference/api/files/delete-v1-files-file-id.md): Delete it. Key scope: `runtime` (or `all`). Next: [`connect`](https://vestra.ai/developers/connect.md) --- url: https://vestra.ai/developers/connect.md --- # connect > Let a user connect Gmail, Slack, Notion and other apps. Let a user connect their apps, such as Gmail, Slack and Notion, so Bash can work in them. A connect link signs one user in to one app from the catalog. Once they agree, Bash can use that app in their work. ## TypeScript ```ts const traveller = vestra.user('traveller-42'); // Get a sign-in link for one app. Send link.url to your user. const link = await traveller.connect('gmail'); console.log(link.status, link.url); // Connect an app for your company. Your company's own Bash will use it, so open the link yourself. const sheets = await vestra.connect('google-sheets'); console.log(sheets.url); // List their apps. You also get the catalog they may pick from. const apps = await traveller.connect.list(); const catalog = await vestra.connect.catalog({ search: 'calendar' }); console.log(apps.data.length, catalog.data.length); // Disconnect an app. await traveller.connect.remove('con_7hs'); ``` ## Python ```python traveller = vestra.user("traveller-42") # Get a sign-in link for one app. Send link.url to your user. link = traveller.connect("gmail") print(link.status, link.url) # Connect an app for your company. Your company's own Bash will use it, so open the link yourself. print(vestra.connect("google-sheets").url) # List their apps. You also get the catalog they may pick from. apps = traveller.connect.list() catalog = vestra.connect.catalog(search="calendar") print(len(apps.data), len(catalog.data)) # Disconnect an app. traveller.connect.remove("con_7hs") ``` ## CLI ```bash # Get a sign-in link for one app. Send it to your user. vestra connect gmail --user traveller-42 # List their apps. You can also list every app they may connect. vestra connect --user traveller-42 vestra connect --catalog --search calendar # Disconnect an app. vestra connect --remove con_7hs --user traveller-42 ``` ## HTTP ```bash # Get a sign-in link for one app. It is for one user: send them its url. curl "$VESTRA_BASE_URL/v1/connections" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "app": "gmail" }' # List their apps. You can also list the catalog your users may pick from. curl "$VESTRA_BASE_URL/v1/connections?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/apps?search=calendar" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Disconnect an app. user_id says whose connection it is. curl -X DELETE "$VESTRA_BASE_URL/v1/connections/con_7hs?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "connect", "arguments": { "user": "traveller-42", "app": "gmail" } } } ``` ## Good to know ### When Bash needs an app during a job If Bash needs an app during a job, it sends an interaction of type `connect`: a request that waits for an answer. Send your user its link, or answer `{ approve: false }` to decline. ### After the user agrees Once your user agrees, they are sent to your `return_url`, or to your company's first allowed return URL. Your webhooks receive `connection.completed` or `connection.failed`, so you know how it went. ### Which apps users can connect `GET /v1/apps` lists the apps your users may connect, with your company's allowlist applied. ## The routes behind connect - `GET` [/v1/apps](https://vestra.ai/developers/reference/api/connected-apps/get-v1-apps.md): The apps your users may connect, after your allowlist. Key scope: `runtime` (or `all`). - `POST` [/v1/connections](https://vestra.ai/developers/reference/api/connected-apps/post-v1-connections.md): Get a connect link for one app, for one user (user_id) or the company's own Bash. After consent the user lands on your return_url (or your first allowed one, or our own page), and a webhook fires. The link carries the id of the connection it makes. Key scope: `runtime` (or `all`). - `GET` [/v1/connections](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections.md): The connected apps of one user (user_id), or of the company's own Bash. Key scope: `runtime` (or `all`). - `GET` [/v1/connections/{connection_id}](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections-connection-id.md): One connection's status. user_id names whose it is. Key scope: `runtime` (or `all`). - `DELETE` [/v1/connections/{connection_id}](https://vestra.ai/developers/reference/api/connected-apps/delete-v1-connections-connection-id.md): Disconnect. user_id names whose it is. Key scope: `runtime` (or `all`). - `GET` [/v1/connect/return](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connect-return.md): Hosted by us. The app sends the user here after consent; we record it and forward them to your return_url. You never call it. Next: [`memory`](https://vestra.ai/developers/memory.md) --- url: https://vestra.ai/developers/memory.md --- # memory > Read, add and delete what your agent remembers about each user. See and change what Bash remembers about each of your users: their preferences, the people in their life, and things they told it once. You can read what it knows, tell it a new fact, or make it forget. ## TypeScript ```ts const traveller = vestra.user('traveller-42'); // Add a fact. Tell Bash something once, and it remembers it. await traveller.memory.add('Prefers a window seat, and never a red-eye.'); // Search what Bash remembers. Ask a question in plain words. const { found, text } = await traveller.memory.search('Which seat does she like?'); console.log(found, text); // List and forget memories. Read the written profile and each memory, newest first. Then forget one, or // everything. const all = await traveller.memory.list(); console.log(all.profile?.markdown); const first = all.data[0]; if (first) await traveller.memory.forget(first.id); await traveller.memory.forgetAll(); ``` ## Python ```python traveller = vestra.user("traveller-42") # Add a fact. Tell Bash something once, and it remembers it. traveller.memory.add("Prefers a window seat, and never a red-eye.") # Search what Bash remembers. Ask a question in plain words. found = traveller.memory.search("Which seat does she like?") print(found.found, found.text) # List and forget memories. Read the written profile and each memory, newest first. Then forget one, or # everything. everything = traveller.memory.list() print(everything.profile) if everything.data: traveller.memory.forget(everything.data[0].id) traveller.memory.forget_all() ``` ## CLI ```bash # Add a fact. Tell Bash something once, and it remembers it. vestra memory add "Prefers a window seat, and never a red-eye." --kind preference --user traveller-42 # Search what Bash remembers. Ask a question in plain words. vestra memory search "Which seat does she like?" --user traveller-42 # List and forget memories. Read the profile and each memory, then forget one, or everything. vestra memory --user traveller-42 vestra memory forget mem_4kx --user traveller-42 vestra memory forget --all --user traveller-42 --yes ``` ## HTTP ```bash # Add a fact. Tell Bash something once, and it remembers it. curl "$VESTRA_BASE_URL/v1/memory" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "text": "Prefers a window seat, and never a red-eye." }' # Search what Bash remembers. Ask a question in plain words. curl "$VESTRA_BASE_URL/v1/memory/search?user_id=traveller-42&q=Which%20seat%20does%20she%20like" \ -H "Authorization: Bearer $VESTRA_API_KEY" # List and forget memories. Read the profile and every memory, newest first. Then forget one, or everything. curl "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory/mem_4ka?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user": "traveller-42", "q": "Which seat does she like?" } } } ``` ## Good to know ### Listing and searching `list` returns a written profile and each memory, newest first. `search` answers a question in plain words, and returns `{ found, text }`. ### Forgetting Forget one memory by its id, or everything Bash knows about a user at once. ### Your company's memory Leave the user out to read and add to the memory of your company's own Bash, the one that works for your company rather than for a user. ### Showing memory in your app A session opened with `memory: true` shows your user what Bash remembers about them, and lets them make it forget. Turn this on for your company first, with `PATCH /v1/org` and `memory_panel: true`. ## The routes behind memory - `GET` [/v1/memory](https://vestra.ai/developers/reference/api/memory/get-v1-memory.md): What Bash remembers about one user (user_id), or the company's own Bash: the profile and the memories, newest first. Key scope: `runtime` (or `all`). - `POST` [/v1/memory](https://vestra.ai/developers/reference/api/memory/post-v1-memory.md): Tell Bash a fact once, for example 'prefers window seats', for one user (user_id) or the company's own Bash. Key scope: `runtime` (or `all`). - `DELETE` [/v1/memory](https://vestra.ai/developers/reference/api/memory/delete-v1-memory.md): Forget everything about one user (user_id), or the company's own Bash. Key scope: `runtime` (or `all`). - `GET` [/v1/memory/search](https://vestra.ai/developers/reference/api/memory/get-v1-memory-search.md): Ask the memory a question (q), for one user (user_id) or the company's own Bash. Key scope: `runtime` (or `all`). - `DELETE` [/v1/memory/{memory_id}](https://vestra.ai/developers/reference/api/memory/delete-v1-memory-memory-id.md): Forget one thing. user_id names whose memory it is. Key scope: `runtime` (or `all`). Next: [`embed`](https://vestra.ai/developers/embed.md) --- url: https://vestra.ai/developers/embed.md --- # embed > Put your agent inside your app, for each signed-in user. Put Bash inside your own app, for the user who is signed in. One iframe gives your user chat, approvals, Bash's live screen and voice. Your server opens a session, a short-lived link for that user, and your page shows its url. ## TypeScript ```ts // Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals, // the live screen and voice. const session = await vestra.user('traveller-42').embed(); // Put session.url in an iframe on your page: print(session.url, session.expires_at) # Show the memory panel. Your company must turn it on first. with_memory = vestra.user("traveller-42").embed(memory=True) # End the session. Its token stops working at once. with_memory.end() ``` ## CLI ```bash # Preview what your users see. It opens chat, approvals, the live screen and voice. In your app, open one # session per visit on your server, with the SDK or over HTTP. vestra embed --user traveller-42 # Show the memory panel. Your company must turn it on first. vestra embed --user traveller-42 --memory ``` ## HTTP ```bash # Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals, # the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an # iframe with allow="microphone". curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }' # Check the session. See whether it is still open. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" # End the session. Its token stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "embed", "arguments": { "user": "traveller-42", "memory": true } } } ``` ## Good to know ### What the session token can see A session's token lasts 15 minutes and can only reach that user and the session's conversation. It never shows what anything costs, never lists interactions, and never sees calls to your tools. ### Which sites can show it Your company's `allowed_embed_origins` setting decides which of your sites may show a session in an iframe. With none set, the session still works as a page of its own. ### Ending a session `DELETE /v1/sessions/{id}` ends a session at once. Its token stops working, its streams close, and a `session.ended` event says so. ## The routes behind embed - `POST` [/v1/sessions](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions.md): Open a session for one of your users (user_id), or for the company's own Bash: chat, a screen to watch or drive, a voice call, and what Bash remembers (memory, when your company turned the panel on). Answers a 15 minute token for the user's browser, a ready page url and the sites allowed to frame it. The token reaches only that user, and only the session's conversation. Key scope: `runtime` (or `all`). - `GET` [/v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id.md): Whether a session is still open, and what it was opened with. Your key, or the session's own token. Key scope: `runtime` (or `all`). - `DELETE` [/v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/delete-v1-sessions-session-id.md): End a session now: its token stops working, its streams close and its call hangs up. Your key, or the session's own token. Key scope: `runtime` (or `all`). - `GET` [/v1/sessions/{session_id}/frame-policy](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-frame-policy.md): Which sites may show a session's page. The page asks this itself; you never need to. Next: [`users`](https://vestra.ai/developers/users.md) --- url: https://vestra.ai/developers/users.md --- # users > Manage your users: profiles, spending caps, export and erasing. Manage the people who use your app. Each one is a user, named by your own id, and Vestra creates them the first time you name them. Here you can give a user a name and time zone, cap what they spend, export everything Vestra holds about them, or erase them. ## TypeScript ```ts // Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat. await vestra.users.create('traveller-42', { name: 'Priya Sharma', timezone: 'Asia/Kolkata' }); // Read their spend. Their profile shows what they spent this month, and what is left under their cap. const traveller = vestra.user('traveller-42'); const profile = await traveller.get(); console.log(profile.spend_month_usd, profile.caps_remaining.month_usd); // Set a monthly cap. Then wake their computer, and export everything Vestra holds about them. await traveller.update({ monthly_cap_usd: 20 }); await traveller.warm(); // wake their computer before they need it console.log(await traveller.export()); // Erase a user. It runs as a job: deleted is false and status is deleting until user.deleted says it is done. const erasing = await traveller.delete(); console.log(erasing.deleted, erasing.status); // List your users. const page = await vestra.users.list(); console.log(page.data.length); ``` ## Python ```python # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat. vestra.users.create("traveller-42", name="Priya Sharma", timezone="Asia/Kolkata") # Read their spend. Their profile shows what they spent this month, and what is left under their cap. traveller = vestra.user("traveller-42") profile = traveller.get() print(profile.spend_month_usd, profile.caps_remaining.month_usd) # Set a monthly cap. Then wake their computer, and export everything Vestra holds about them. traveller.update(monthly_cap_usd=20) traveller.warm() # wake their computer before they need it print(traveller.export()) # Erase a user. It runs as a job: deleted is False and status is deleting until user.deleted says it is done. erasing = traveller.delete() print(erasing.deleted, erasing.status) # List your users. print(len(vestra.users.list().data)) ``` ## CLI ```bash # List your users. vestra users # Read their spend. You see what one user spent, and what is left this month. vestra users traveller-42 # Set a monthly cap. Then export everything Vestra holds about them, and wake their computer. vestra users traveller-42 --cap 20 vestra users export traveller-42 vestra users warm traveller-42 # Erase a user. It runs as a job, and user.deleted says when it is done. vestra users delete traveller-42 --yes ``` ## HTTP ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` ## Good to know ### User ids Your id for a user is 1 to 128 characters and starts with a letter or digit. Reading a user never creates one. An unknown id on a read returns 404. ### Erasing a user `DELETE /v1/users/{id}` returns 202 with `deleted: false` and `status: deleting`. Vestra erases their computer, files and memory in the background, and the `user.deleted` event says when it is done. ### Spending A user's record shows what they spent today and this month, and how much they can still spend under their caps. ### The key you need Exporting or erasing a user needs a key with the `admin` scope, or `all`. ### A page's own data, with no model `POST /v1/users/{id}/browser/fetch` opens one page in a tab of the user's browser, with their sign-ins, and gives back the bodies of the page's own data calls that match `capture`. It can click, type, scroll and wait first (`steps`), and call the deployment's sites from inside the page (`page_fetch`). `keep_open_ms` keeps the tab, so the next fetch sends no `url` and runs there at once. It opens only sites in `browser.allowed_domains`, never a page in `browser.blocked_paths` such as a payment page (403 `page_blocked`), yields to a person with 409 `browser_in_use`, and answers a slow page with 504 `timeout` and what came in `partial`. `POST /v1/users/{id}/warm` with `keep_ms` keeps the computer awake for the next one. ## The routes behind users - `POST` [/v1/users](https://vestra.ai/developers/reference/api/users/post-v1-users.md): Create one of your users, or return the existing one. Safe to repeat with the same user_id. Key scope: `admin` (or `all`). - `GET` [/v1/users](https://vestra.ai/developers/reference/api/users/get-v1-users.md): List your users. Key scope: `runtime` (or `all`). - `GET` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/get-v1-users-user-id.md): One user: status, profile, memory mode, default deployment, and what they spent today and this month against their caps. Key scope: `runtime` (or `all`). - `PATCH` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/patch-v1-users-user-id.md): Change name, language, time zone, metadata, day or month spend cap, default deployment, or status (active or disabled). Key scope: `admin` (or `all`). - `DELETE` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/delete-v1-users-user-id.md): Erase the user and everything Bash holds for them: computer, files, memory. Runs as a job; a webhook says when it is done. Key scope: `admin` (or `all`). - `POST` [/v1/users/{user_id}/export](https://vestra.ai/developers/reference/api/users/post-v1-users-user-id-export.md): Package everything held for the user into one download, for data-rights requests. Key scope: `admin` (or `all`). - `POST` [/v1/users/{user_id}/warm](https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-warm.md): Wake the user's computer now (for example when they open your app), so their next task starts without waiting. Answers 200 when it is already up and 202 while it wakes. At most 10 computers wake at once per company (429 warm_limit), and a second call for one user within a minute rides the first. keep_ms keeps it awake that long once it is up, up to two minutes; calling again extends it. Key scope: `runtime` (or `all`). - `POST` [/v1/users/{user_id}/browser/fetch](https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-browser-fetch.md): Open one page in a background tab of the user's own browser, with no model, and get back the bodies of the page's own data calls that match capture. Only on sites the user's deployment lists in browser.allowed_domains (403 domain_not_allowed), and never on a page in its browser.blocked_paths, such as a payment page (403 page_blocked). Wakes the computer when it sleeps. Answers as soon as every required capture has its responses, with timings; 409 browser_in_use while a person drives the browser, 409 browser_busy past four at once; 504 timeout carries what came back in partial. frames returns small pictures of the tab at each milestone. A page_fetch step calls one of the deployment's sites from inside the page, as the page's own script would; keep_open_ms keeps the tab for the next fetch, which sends no url (409 no_open_tab when none is kept). Key scope: `runtime` (or `all`). - `GET` [/v1/users/{user_id}/browser/cookies](https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-cookies.md): Which cookies of some names the user's browser holds for one site, and until when: names, sites and expiry only, never a value. Only a site on the deployment's browser.allowed_domains (403 domain_not_allowed). Opens no page and acts on nothing, so it never disturbs a person using the browser: read it every few seconds to see a person finish signing in on a page you handed them (front), as the site's session cookie appears or its expiry moves on. Wakes the computer when it sleeps. Key scope: `runtime` (or `all`). Next: [`usage`](https://vestra.ai/developers/usage.md) --- url: https://vestra.ai/developers/usage.md --- # usage > See what each job and user cost, and set caps and alerts. See what every job and every user cost, in billed US dollars, grouped the way you need. You can also set spending caps and alerts. Line items are the itemized receipt for one conversation or one task, ready to show your user. ## TypeScript ```ts // See costs by user. This is what everything cost since the 1st, in billed US dollars. const byUser = await vestra.usage({ from: '2026-10-01', group_by: 'user' }); console.log(byUser.total.spend_usd); // See one user's spend. const theirs = await vestra.user('traveller-42').usage(); console.log(theirs.total.spend_usd); // Get a conversation's receipt. It lists every charge. const receipt = await vestra.usage.lineItems({ conversation: 'conv_8f2k' }); console.log(receipt.data.length); // Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. await vestra.usage.caps({ user_monthly_usd: 20, alert_thresholds: [50, 80] }); ``` ## Python ```python # See costs by user. This is what everything cost since the 1st, in billed US dollars. by_user = vestra.usage(from_="2026-10-01", group_by="user") print(by_user.total.spend_usd) # See one user's spend. print(vestra.user("traveller-42").usage().total.spend_usd) # Get a conversation's receipt. It lists every charge. print(len(vestra.usage.line_items(conversation="conv_8f2k").data)) # Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. vestra.usage.caps(user_monthly_usd=20, alert_thresholds=[50, 80]) ``` ## CLI ```bash # See costs by user. This is what everything cost since the 1st. vestra usage --from 2026-10-01 --by user # Get a conversation's receipt. It lists every charge. vestra usage items conv_8f2k # Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. vestra usage caps --customer-monthly 20 --alerts 50,80 ``` ## HTTP ```bash # See costs by user. This is what everything cost since the 1st, in billed US dollars. curl "$VESTRA_BASE_URL/v1/usage?from=2026-10-01&group_by=user" \ -H "Authorization: Bearer $VESTRA_API_KEY" # See one user's spend. curl "$VESTRA_BASE_URL/v1/usage?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get a conversation's receipt. It lists every charge, newest first. curl "$VESTRA_BASE_URL/v1/usage/line-items?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_monthly_usd": 20, "alert_thresholds": [50, 80] }' ``` ## MCP ```json { "method": "tools/call", "params": { "name": "usage", "arguments": { "from": "2026-10-01", "group_by": "user" } } } ``` ## Good to know ### Grouping costs `group_by` can be `day`, `user`, `deployment`, `conversation`, `task`, `model` or `category`. `from` and `to` are dates. ### Caps and alerts You can set daily and monthly caps, for your company as a whole and for each user, and alert thresholds as percentages of a cap. Your webhooks receive `usage.threshold_reached` and `usage.cap_reached`. ### Late charges A task's last charges can arrive a few seconds after it ends. `cost_complete: false` means more charges are coming. You see it on `message.completed` and on a run's `cost()`. ## The routes behind usage - `GET` [/v1/limits](https://vestra.ai/developers/reference/api/account/get-v1-limits.md): The day and month caps for the company and per user, the alert thresholds and the rate limits. Key scope: `runtime` (or `all`). - `PATCH` [/v1/limits](https://vestra.ai/developers/reference/api/account/patch-v1-limits.md): Set the day and month caps for the company and per user, and the alert thresholds, within what Vestra allows. Key scope: `admin` (or `all`). - `GET` [/v1/usage](https://vestra.ai/developers/reference/api/account/get-v1-usage.md): Exact billed spend and real task counts, by day, user, deployment, conversation, task, model or kind of spend, filtered by user, deployment or conversation. An erased user's spend stays in the totals, as the user unknown. Key scope: `runtime` (or `all`). - `GET` [/v1/usage/line-items](https://vestra.ai/developers/reference/api/account/get-v1-usage-line-items.md): The charges behind one task or one conversation, newest first: the receipt you can show your user. Key scope: `runtime` (or `all`). Next: [`keys`](https://vestra.ai/developers/keys.md) --- url: https://vestra.ai/developers/keys.md --- # keys > Create, roll and revoke your test and live API keys. Create, roll and revoke the API keys your servers use to call Vestra. Use test keys while you build, and live keys for real users. Each key has scopes that limit what it can do, so a key that can only set things up can never act for a user. ## TypeScript ```ts // Check your key. See your company, this key, and whether it is test or live. const me = await vestra.keys.me(); console.log(me.org.name, me.mode); // Create a key for CI. This one can only set things up. Its secret is shown only once. const ci = await vestra.keys.create({ name: 'ci', scopes: ['setup'] }); console.log(ci.secret); // Roll a key. You get a new secret, and the old one keeps working for an hour. const rolled = await vestra.keys.roll(ci.id, { grace_seconds: 3600 }); console.log(rolled.key.secret); // List your keys. The list shows only the last four characters of each. const keys = await vestra.keys.list(); console.log(keys.data.map((k) => k.last4)); // Revoke a key. It stops working at once. await vestra.keys.revoke(ci.id); ``` ## Python ```python # Check your key. See your company, this key, and whether it is test or live. me = vestra.keys.me() print(me.org.name, me.mode) # Create a key for CI. This one can only set things up. Its secret is shown only once. ci = vestra.keys.create(name="ci", scopes=["setup"]) print(ci.secret) # Roll a key. You get a new secret, and the old one keeps working for an hour. rolled = vestra.keys.roll(ci.id, grace_seconds=3600) print(rolled.key.secret) # List your keys. The list shows only the last four characters of each. print([k.last4 for k in vestra.keys.list().data]) # Revoke a key. It stops working at once. vestra.keys.revoke(ci.id) ``` ## CLI ```bash # List your keys. vestra keys # Create a key for CI. This one can only set things up. Its secret is shown only once. vestra keys create --name ci --scopes setup # Roll a key. You get a new secret, and the old one keeps working for an hour. vestra keys roll key_8sd --grace 3600 # Revoke a key. It stops working at once. vestra keys revoke key_8sd ``` ## HTTP ```bash # Check your key. See your company, this key, and whether it is test or live. curl "$VESTRA_BASE_URL/v1/me" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Create a key for CI. This one can only set things up. Its secret is shown only once. curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Roll a key. You get a new secret, and the old one keeps working for an hour. curl "$VESTRA_BASE_URL/v1/keys/key_3vd/roll" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "grace_seconds": 3600 }' # Revoke a key. It stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/keys/key_3vd" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP Only people manage keys: no MCP tool creates, rolls or revokes one. Use the console, the CLI or HTTP. ## Good to know ### Scopes A key's scope is `all`, the default, or any of these: `runtime` (ask, answer, events, files, sessions, connect, memory, routines), `setup` (deployments, tests, secrets, webhooks) and `admin` (keys, limits, your company's settings, and exporting or erasing a user). A key without the scope a route needs gets `403 scope_missing`. Every route in these docs names the scope it needs. ### Test and live A key's prefix shows its mode: `vk_test_` or `vk_live_`. A new key gets the same mode as the key that created it. A test key only sees test users, test deployments and test money. ### Checking a key Any key can call `GET /v1/me`, whatever its scopes. It returns your company, the key and its mode. ### Rolling a key A new key's secret is shown only once. When you roll a key, the old one keeps working for the grace period you choose, or for a day if you choose none. ## The routes behind keys - `GET` [/v1/me](https://vestra.ai/developers/reference/api/account/get-v1-me.md): Who is calling: the company, the key, and whether it is test or live. Any key may call it. - `GET` [/v1/org](https://vestra.ai/developers/reference/api/account/get-v1-org.md): The company account: name, status, the wallet in billed dollars, day and month caps, alert thresholds, allowed return URLs, allowed embed origins, default deployment. Key scope: `admin` (or `all`). - `PATCH` [/v1/org](https://vestra.ai/developers/reference/api/account/patch-v1-org.md): Change the name, allowed return URLs, allowed embed origins, default deployment or default memory mode. Key scope: `admin` (or `all`). - `GET` [/v1/keys](https://vestra.ai/developers/reference/api/account/get-v1-keys.md): List keys. Only the last four characters are ever shown again. Key scope: `admin` (or `all`). - `POST` [/v1/keys](https://vestra.ai/developers/reference/api/account/post-v1-keys.md): Create a test or live key. The full key is shown once. Key scope: `admin` (or `all`). - `POST` [/v1/keys/{key_id}/roll](https://vestra.ai/developers/reference/api/account/post-v1-keys-key-id-roll.md): Replace a key. The old one keeps working for a grace period you choose, then stops. Key scope: `admin` (or `all`). - `DELETE` [/v1/keys/{key_id}](https://vestra.ai/developers/reference/api/account/delete-v1-keys-key-id.md): Revoke a key immediately. Key scope: `admin` (or `all`). - `GET` [/v1/audit-log](https://vestra.ai/developers/reference/api/account/get-v1-audit-log.md): Every key use, settings change and approval answer, newest first. Key scope: `admin` (or `all`). - `GET` [/v1/models](https://vestra.ai/developers/reference/api/account/get-v1-models.md): The models a deployment may choose from. Key scope: `runtime` (or `all`). - `GET` [/v1/health](https://vestra.ai/developers/reference/api/account/get-v1-health.md): Is the API up. No key needed. - `GET` [/v1/openapi.json](https://vestra.ai/developers/reference/api/contract/get-v1-openapi-json.md): The whole contract, machine-readable. Next: [`secrets`](https://vestra.ai/developers/secrets.md) --- url: https://vestra.ai/developers/secrets.md --- # secrets > Store values your agent uses but never shows, such as tokens. Store values that Bash uses but never shows, such as your MCP servers' tokens or your users' own credentials. Secrets are write-only: once stored, a secret is never shown again, not even to you. ## TypeScript ```ts // Store a company secret. Your MCP servers refer to it as ${secret:TRIPNEST_MCP_TOKEN}. await vestra.secrets.set('TRIPNEST_MCP_TOKEN', process.env.TRIPNEST_MCP_TOKEN!); // Store a user's secret. It is for their own account, as ${user_secret:IRCTC_TOKEN}. await vestra.user('traveller-42').secrets.set('IRCTC_TOKEN', irctcToken); // List and delete secrets. The list shows names only, never values. const names = await vestra.secrets.list(); console.log(names.data.map((s) => s.name)); await vestra.secrets.delete('TRIPNEST_MCP_TOKEN'); ``` ## Python ```python # Store a company secret. Your MCP servers refer to it as ${secret:TRIPNEST_MCP_TOKEN}. vestra.secrets.set("TRIPNEST_MCP_TOKEN", os.environ["TRIPNEST_MCP_TOKEN"]) # Store a user's secret. It is for their own account, as ${user_secret:IRCTC_TOKEN}. vestra.user("traveller-42").secrets.set("IRCTC_TOKEN", irctc_token) # List and delete secrets. The list shows names only, never values. print([s.name for s in vestra.secrets.list().data]) vestra.secrets.delete("TRIPNEST_MCP_TOKEN") ``` ## CLI ```bash # Store a company secret. Type the value when asked. From a script, pass --value - with the value on stdin, or # --value-from-env . The value is never part of the command, where it would stay in your shell history. vestra secrets set TRIPNEST_MCP_TOKEN # Store a user's secret. It is for their own account. vestra secrets set IRCTC_TOKEN --user traveller-42 # List and delete secrets. The list shows names only, never values. vestra secrets vestra secrets delete TRIPNEST_MCP_TOKEN ``` ## HTTP ```bash # Store a company secret. Your MCP servers refer to it as ${secret:TRIPNEST_MCP_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "your-token" }' # Store a user's secret. It is for their own account, as ${user_secret:IRCTC_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/IRCTC_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "value": "token-from-their-account" }' # List and delete secrets. The list shows names only, never values. curl "$VESTRA_BASE_URL/v1/secrets" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` ## MCP Only people manage secrets: no MCP tool stores or lists one. Use the console, the CLI or HTTP. ## Good to know ### Company and user secrets A secret stored without a user belongs to your company. Refer to it as `${secret:NAME}`. A secret stored with a user belongs to that user. Refer to it as `${user_secret:NAME}`. ### Listing secrets Listing secrets returns their names, never their values. ### The key you need Managing secrets needs a key with the `setup` scope, or `all`. ## The routes behind secrets - `PUT` [/v1/secrets/{name}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/put-v1-secrets-name.md): Store a secret, write-only: the company's (refer to it as ${secret:NAME}), or with user_id one user's own (${user_secret:NAME}), for example their travel account token. Storing it again replaces it. Key scope: `setup` (or `all`). - `DELETE` [/v1/secrets/{name}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-secrets-name.md): Remove one: the company's, or one user's (user_id). Key scope: `setup` (or `all`). - `GET` [/v1/secrets](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-secrets.md): Secret names, never the values: the company's, or one user's (user_id). Key scope: `setup` (or `all`). Next: [`mcp`](https://vestra.ai/developers/mcp.md) --- url: https://vestra.ai/developers/mcp.md --- # mcp > Use Vestra from any editor or agent that speaks MCP. Use Vestra from any editor or AI agent that speaks MCP, the Model Context Protocol. There is one MCP endpoint, `/v1/mcp`, and any MCP client can call it with your API key. `tools/list` shows the tools it offers. To let a coding agent search these docs without a key, add the [docs MCP](https://vestra.ai/developers/docs-mcp). [Build with AI](https://vestra.ai/developers/build-with-ai.md) lists every resource for coding agents. ## TypeScript MCP is a separate way in, so there is nothing to call from the TypeScript SDK. Point any MCP client at `https://api.vestra.ai/v1/mcp` (during the preview, `https://api-staging.vestra.ai/v1/mcp`), with your key as a Bearer token. ## Python MCP is a separate way in, so there is nothing to call from the Python SDK. Point any MCP client at `https://api.vestra.ai/v1/mcp` (during the preview, `https://api-staging.vestra.ai/v1/mcp`), with your key as a Bearer token. ## CLI `vestra mcp` runs a local MCP server for your editor over stdio, using the key `vestra login` saved. Add it to your editor as a stdio server, with command `vestra` and args `["mcp"]`. ## HTTP ```bash # Connect any MCP client. Point it at this URL with your key. Over plain HTTP the endpoint speaks JSON-RPC, # and it needs both types in the Accept header. tools/list shows the tools it offers. curl "$VESTRA_BASE_URL/v1/mcp" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' # Connect an agent that speaks A2A. Each deployment has an agent card, and reading it needs no key. curl "$VESTRA_BASE_URL/v1/a2a/dep_4kq/.well-known/agent-card.json" ``` ## MCP ```json { "mcpServers": { "vestra": { "type": "http", "url": "https://api-staging.vestra.ai/v1/mcp", "headers": { "Authorization": "Bearer vk_test_..." } } } } ``` ## Good to know ### Connecting a client The endpoint uses MCP's streamable HTTP transport. Send your key in `Authorization: Bearer`, and send `Accept: application/json, text/event-stream`. ### The key you need MCP needs a key with the `runtime` scope, or `all`. ### Agents that speak A2A Each deployment also answers A2A, the Agent2Agent protocol, at `/v1/a2a/{deployment_id}`. Its agent card is at `/v1/a2a/{deployment_id}/.well-known/agent-card.json`. ## The routes behind mcp - `POST` [/v1/mcp](https://vestra.ai/developers/reference/api/mcp-server/post-v1-mcp.md): One MCP endpoint (streamable HTTP): the card's lines as tools, each with an optional user (with none, your company's own Bash). Keys and secrets stay with people. Key scope: `runtime` (or `all`). - `GET` [/v1/a2a/{deployment_id}/.well-known/agent-card.json](https://vestra.ai/developers/reference/api/a2a/get-v1-a2a-deployment-id-well-known-agent-card-json.md): What this deployment does and how to authenticate, for other agents to discover. - `POST` [/v1/a2a/{deployment_id}](https://vestra.ai/developers/reference/api/a2a/post-v1-a2a-deployment-id.md): A2A JSON-RPC: send a message, stream, get, list or cancel. Push notifications are not offered; subscribe to webhooks instead. An A2A exchange is a conversation; an A2A task is its work. Key scope: `runtime` (or `all`). --- url: https://vestra.ai/developers/reference.md --- # Reference > The exact shape of everything, read from the code that runs it: plain words first, then the fields. ## [API](https://vestra.ai/developers/reference/api.md) Every route: what it does, every field it takes and answers, and a sample in your language. 134 routes in 21 groups. - [Account](https://vestra.ai/developers/reference/api/account.md) - [Users](https://vestra.ai/developers/reference/api/users.md) - [Conversations](https://vestra.ai/developers/reference/api/conversations.md) - [Work](https://vestra.ai/developers/reference/api/work.md) - [Interactions](https://vestra.ai/developers/reference/api/interactions.md) - [Files](https://vestra.ai/developers/reference/api/files.md) - [Connected apps](https://vestra.ai/developers/reference/api/connected-apps.md) - [MCP servers and secrets](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md) - [Skills](https://vestra.ai/developers/reference/api/skills.md) - [Notebooks](https://vestra.ai/developers/reference/api/notebooks.md) - [Memory](https://vestra.ai/developers/reference/api/memory.md) - [Deployments](https://vestra.ai/developers/reference/api/deployments.md) - [Tests](https://vestra.ai/developers/reference/api/tests.md) - [Computer setup](https://vestra.ai/developers/reference/api/computer-setup.md) - [Routines](https://vestra.ai/developers/reference/api/routines.md) - [Batches](https://vestra.ai/developers/reference/api/batches.md) - [Webhooks and events](https://vestra.ai/developers/reference/api/webhooks-and-events.md) - [Sessions](https://vestra.ai/developers/reference/api/sessions.md) - [MCP server](https://vestra.ai/developers/reference/api/mcp-server.md) - [A2A](https://vestra.ai/developers/reference/api/a2a.md) - [Contract](https://vestra.ai/developers/reference/api/contract.md) ## [vestra.yaml](https://vestra.ai/developers/reference/file.md) Every field of the file that sets up your Bash: what it does, its type and default, and an example. 62 fields. ## [Events](https://vestra.ai/developers/reference/events.md) Every event on the stream and in your webhooks, and every field it carries. 34 events. ## [Errors](https://vestra.ai/developers/reference/errors.md) Every error code the API answers: its status, what it means, and what to do. 251 codes. ## [SDKs and CLI](https://vestra.ai/developers/reference/sdk.md) Every method of the TypeScript and Python SDKs, and every command of the CLI. TypeScript, Python and the CLI. For tools: the contract as [an OpenAPI explorer](https://vestra.ai/developers/reference/openapi), its [`v1.json`](https://vestra.ai/developers/v1.json), and vestra.yaml's [`JSON Schema`](https://vestra.ai/developers/vestra.schema.json) for your editor. --- url: https://vestra.ai/developers/reference/api.md --- # API reference > Every route, by what it is for. Each group's page says what each route does, every field it takes and answers, and shows it in your language. Send `Authorization: Bearer vk_test_...` on every call. ## [Account](https://vestra.ai/developers/reference/api/account.md) Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. - `GET` [/v1/me](https://vestra.ai/developers/reference/api/account/get-v1-me.md): Who is calling: the company, the key, and whether it is test or live. - `GET` [/v1/org](https://vestra.ai/developers/reference/api/account/get-v1-org.md): The company account: name, status, the wallet in billed dollars, day and month caps, alert thresholds, allowed return URLs, allowed embed origins, default deployment. - `PATCH` [/v1/org](https://vestra.ai/developers/reference/api/account/patch-v1-org.md): Change the name, allowed return URLs, allowed embed origins, default deployment or default memory mode. - `GET` [/v1/keys](https://vestra.ai/developers/reference/api/account/get-v1-keys.md): List keys. - `POST` [/v1/keys](https://vestra.ai/developers/reference/api/account/post-v1-keys.md): Create a test or live key. - `POST` [/v1/keys/{key_id}/roll](https://vestra.ai/developers/reference/api/account/post-v1-keys-key-id-roll.md): Replace a key. - `DELETE` [/v1/keys/{key_id}](https://vestra.ai/developers/reference/api/account/delete-v1-keys-key-id.md): Revoke a key immediately. - `GET` [/v1/limits](https://vestra.ai/developers/reference/api/account/get-v1-limits.md): The day and month caps for the company and per user, the alert thresholds and the rate limits. - `PATCH` [/v1/limits](https://vestra.ai/developers/reference/api/account/patch-v1-limits.md): Set the day and month caps for the company and per user, and the alert thresholds, within what Vestra allows. - `GET` [/v1/usage](https://vestra.ai/developers/reference/api/account/get-v1-usage.md): Exact billed spend and real task counts, by day, user, deployment, conversation, task, model or kind of spend, filtered by user, deployment or conversation. - `GET` [/v1/usage/line-items](https://vestra.ai/developers/reference/api/account/get-v1-usage-line-items.md): The charges behind one task or one conversation, newest first: the receipt you can show your user. - `GET` [/v1/audit-log](https://vestra.ai/developers/reference/api/account/get-v1-audit-log.md): Every key use, settings change and approval answer, newest first. - `GET` [/v1/models](https://vestra.ai/developers/reference/api/account/get-v1-models.md): The models a deployment may choose from. - `GET` [/v1/health](https://vestra.ai/developers/reference/api/account/get-v1-health.md): Is the API up. ## [Users](https://vestra.ai/developers/reference/api/users.md) Your users: set one up, read their spend, change their cap, export or erase them. - `POST` [/v1/users](https://vestra.ai/developers/reference/api/users/post-v1-users.md): Create one of your users, or return the existing one. - `GET` [/v1/users](https://vestra.ai/developers/reference/api/users/get-v1-users.md): List your users. - `GET` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/get-v1-users-user-id.md): One user: status, profile, memory mode, default deployment, and what they spent today and this month against their caps. - `PATCH` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/patch-v1-users-user-id.md): Change name, language, time zone, metadata, day or month spend cap, default deployment, or status (active or disabled). - `DELETE` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/delete-v1-users-user-id.md): Erase the user and everything Bash holds for them: computer, files, memory. - `POST` [/v1/users/{user_id}/export](https://vestra.ai/developers/reference/api/users/post-v1-users-user-id-export.md): Package everything held for the user into one download, for data-rights requests. ## [Conversations](https://vestra.ai/developers/reference/api/conversations.md) Ask Bash in a conversation, follow its reply as it happens, and stop it. - `POST` [/v1/conversations](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations.md): Ask. - `GET` [/v1/conversations](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations.md): Conversations, newest first: one user's with user_id, or every one of the company's without it (the company's own Bash shows user_id null). - `GET` [/v1/conversations/{conversation_id}](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id.md): One conversation: status (idle, replying, working, waiting), its open work, its open approvals. - `POST` [/v1/conversations/{conversation_id}/messages](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-messages.md): Send a message. - `GET` [/v1/conversations/{conversation_id}/messages](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-messages.md): The conversation, paged: your messages, Bash's replies, and summaries of the work it did. - `GET` [/v1/conversations/{conversation_id}/events](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-events.md): The one stream (SSE): replies as they are typed, work started and finished, progress, approvals, tool calls, files. - `POST` [/v1/conversations/{conversation_id}/stop](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-stop.md): Stop the reply and every piece of work the conversation started. ## [Work](https://vestra.ai/developers/reference/api/work.md) The work Bash does on its computer: list it, read it, stop it or delete it. - `GET` [/v1/tasks](https://vestra.ai/developers/reference/api/work/get-v1-tasks.md): All work across your users, or one user's with user_id. - `GET` [/v1/tasks/{task_id}](https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id.md): Status, title, the progress list, the result (text, and data when an output_schema was set), the outcome, cost so far. - `DELETE` [/v1/tasks/{task_id}](https://vestra.ai/developers/reference/api/work/delete-v1-tasks-task-id.md): Delete it and its outputs. - `GET` [/v1/tasks/{task_id}/trace](https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id-trace.md): Every step: tools called with a summary of inputs and outputs, timings, tokens and cost per turn. - `POST` [/v1/tasks/{task_id}/stop](https://vestra.ai/developers/reference/api/work/post-v1-tasks-task-id-stop.md): Stop this piece of work only. ## [Interactions](https://vestra.ai/developers/reference/api/interactions.md) What Bash waits on (approvals, questions, apps to connect, hand-offs, calls to your tools) and the answers. - `GET` [/v1/interactions](https://vestra.ai/developers/reference/api/interactions/get-v1-interactions.md): Everything Bash waits on, across your users: approvals, questions, app connections, hand-offs, and calls to your client tools (type tool_call). - `GET` [/v1/interactions/{interaction_id}](https://vestra.ai/developers/reference/api/interactions/get-v1-interactions-interaction-id.md): One item, by its int_ or call_ id: its type (approval, question, connect, handoff, tool_call), what Bash wants to do, the options, when it expires. - `POST` [/v1/interactions/{interaction_id}/answer](https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-answer.md): Answer it, whatever it is. - `POST` [/v1/interactions/{interaction_id}/cancel](https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-cancel.md): Withdraw it. ## [Files](https://vestra.ai/developers/reference/api/files.md) Files Bash makes, and files you hand it. - `POST` [/v1/files](https://vestra.ai/developers/reference/api/files/post-v1-files.md): Upload a file for Bash to use: multipart/form-data with one part named file. - `GET` [/v1/files](https://vestra.ai/developers/reference/api/files/get-v1-files.md): Files, each with its task and conversation: one user's (user_id) or the company's own Bash's, one task's outputs (task_id), or one conversation's, its tasks' outputs (conversation_id). - `GET` [/v1/files/{file_id}](https://vestra.ai/developers/reference/api/files/get-v1-files-file-id.md): Details and a short-lived download link. - `DELETE` [/v1/files/{file_id}](https://vestra.ai/developers/reference/api/files/delete-v1-files-file-id.md): Delete it. ## [Connected apps](https://vestra.ai/developers/reference/api/connected-apps.md) The apps each user connects through a sign-in link, and the apps they may choose from. - `GET` [/v1/apps](https://vestra.ai/developers/reference/api/connected-apps/get-v1-apps.md): The apps your users may connect, after your allowlist. - `POST` [/v1/connections](https://vestra.ai/developers/reference/api/connected-apps/post-v1-connections.md): Get a connect link for one app, for one user (user_id) or the company's own Bash. - `GET` [/v1/connections](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections.md): The connected apps of one user (user_id), or of the company's own Bash. - `GET` [/v1/connections/{connection_id}](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections-connection-id.md): One connection's status. - `DELETE` [/v1/connections/{connection_id}](https://vestra.ai/developers/reference/api/connected-apps/delete-v1-connections-connection-id.md): Disconnect. - `GET` [/v1/connect/return](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connect-return.md): Hosted by us. ## [MCP servers and secrets](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md) Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. - `GET` [/v1/mcp-servers](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers.md): Your registered MCP servers. - `POST` [/v1/mcp-servers](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers.md): Register one: URL, auth (none, or headers using ${secret:NAME} or ${user_secret:NAME}), and its tool policy floor: tools never run and tools that always need approval, for every deployment in both modes. - `GET` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id.md): One server and its settings. - `PATCH` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/patch-v1-mcp-servers-server-id.md): Change URL, auth or the tool policy floor, for every deployment in both modes. - `DELETE` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-mcp-servers-server-id.md): Remove it from every user. - `POST` [/v1/mcp-servers/{server_id}/check](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers-server-id-check.md): Connect now and list its tools, to prove it works. - `GET` [/v1/mcp-servers/{server_id}/tools](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id-tools.md): The tools we found, each with its setting in the server's tool policy floor. - `PUT` [/v1/secrets/{name}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/put-v1-secrets-name.md): Store a secret, write-only: the company's (refer to it as ${secret:NAME}), or with user_id one user's own (${user_secret:NAME}), for example their travel account token. - `DELETE` [/v1/secrets/{name}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-secrets-name.md): Remove one: the company's, or one user's (user_id). - `GET` [/v1/secrets](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-secrets.md): Secret names, never the values: the company's, or one user's (user_id). ## [Skills](https://vestra.ai/developers/reference/api/skills.md) Skills your Bash may use: upload one, give it new versions, list them. - `GET` [/v1/skills](https://vestra.ai/developers/reference/api/skills/get-v1-skills.md): Your company skills, plus the skills Bash already has. - `POST` [/v1/skills](https://vestra.ai/developers/reference/api/skills/post-v1-skills.md): Upload a skill folder as a zip, or as JSON files. - `GET` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id.md): One skill and its files. - `PUT` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/put-v1-skills-skill-id.md): Upload a new version. - `DELETE` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/delete-v1-skills-skill-id.md): Remove it from every user. - `GET` [/v1/skills/{skill_id}/versions](https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id-versions.md): Every version, each with a content hash. ## [Notebooks](https://vestra.ai/developers/reference/api/notebooks.md) Knowledge Bash reads before it answers: notebooks, and the documents in them. - `POST` [/v1/notebooks](https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks.md): Create a notebook. - `GET` [/v1/notebooks](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks.md): Your notebooks. - `GET` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id.md): One notebook and who may read it. - `PATCH` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/patch-v1-notebooks-notebook-id.md): Rename it, or set who may read it: every user, or named users only. - `DELETE` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id.md): Delete it. - `POST` [/v1/notebooks/{notebook_id}/documents](https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks-notebook-id-documents.md): Add a document, as a file or as text. - `GET` [/v1/notebooks/{notebook_id}/documents](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id-documents.md): Its documents. - `DELETE` [/v1/notebooks/{notebook_id}/documents/{document_id}](https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id-documents-document-id.md): Remove one. ## [Memory](https://vestra.ai/developers/reference/api/memory.md) What Bash remembers about each user: read it, search it, add to it, forget it. - `GET` [/v1/memory](https://vestra.ai/developers/reference/api/memory/get-v1-memory.md): What Bash remembers about one user (user_id), or the company's own Bash: the profile and the memories, newest first. - `POST` [/v1/memory](https://vestra.ai/developers/reference/api/memory/post-v1-memory.md): Tell Bash a fact once, for example 'prefers window seats', for one user (user_id) or the company's own Bash. - `DELETE` [/v1/memory](https://vestra.ai/developers/reference/api/memory/delete-v1-memory.md): Forget everything about one user (user_id), or the company's own Bash. - `GET` [/v1/memory/search](https://vestra.ai/developers/reference/api/memory/get-v1-memory-search.md): Ask the memory a question (q), for one user (user_id) or the company's own Bash. - `DELETE` [/v1/memory/{memory_id}](https://vestra.ai/developers/reference/api/memory/delete-v1-memory-memory-id.md): Forget one thing. ## [Deployments](https://vestra.ai/developers/reference/api/deployments.md) How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. - `GET` [/v1/deployments](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments.md): Your deployments. - `POST` [/v1/deployments](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments.md): Create one on base 'bash'. - `GET` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id.md): One deployment, by its id or its name: the live version and the draft. - `PUT` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/put-v1-deployments-deployment-id.md): Apply a whole vestra.yaml, sent as JSON, by the deployment's name: the first call makes it, and a file that changes nothing makes no version (changed: []). - `PATCH` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/patch-v1-deployments-deployment-id.md): Rename, describe or archive it. - `POST` [/v1/deployments/{deployment_id}/versions](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions.md): Save a new version: instructions, language, MCP servers and this version's own tool policy for them (mcp_tool_policy, on top of each server's floor), allowed apps, approval rules, model, memory mode, notebooks, limits per task. - `GET` [/v1/deployments/{deployment_id}/versions](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions.md): Every version. - `GET` [/v1/deployments/{deployment_id}/versions/{version}](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions-version.md): One version, in full. - `GET` [/v1/deployments/{deployment_id}/diff](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-diff.md): What changed between two versions (?from=&to=). - `POST` [/v1/deployments/{deployment_id}/versions/{version}/promote](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions-version-promote.md): Make it live. - `POST` [/v1/deployments/{deployment_id}/rollback](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-rollback.md): Put an earlier version back live, instantly. - `POST` [/v1/deployments/interview](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-interview.md): Build a deployment by talking to Bash: describe the job, answer its questions, receive a draft version. ## [Tests](https://vestra.ai/developers/reference/api/tests.md) The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. - `GET` [/v1/deployments/{deployment_id}/scenarios](https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-scenarios.md): The deployment's test scenarios. - `POST` [/v1/deployments/{deployment_id}/scenarios](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios.md): Add one: a pretend user, their goal, what success looks like, what must never happen. - `POST` [/v1/deployments/{deployment_id}/scenarios/generate](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios-generate.md): Have Bash draft scenarios from the deployment's instructions. - `PATCH` [/v1/deployments/{deployment_id}/scenarios/{scenario_id}](https://vestra.ai/developers/reference/api/tests/patch-v1-deployments-deployment-id-scenarios-scenario-id.md): Change one. - `DELETE` [/v1/deployments/{deployment_id}/scenarios/{scenario_id}](https://vestra.ai/developers/reference/api/tests/delete-v1-deployments-deployment-id-scenarios-scenario-id.md): Remove one. - `POST` [/v1/deployments/{deployment_id}/test-runs](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-test-runs.md): Run every scenario against a version: version, or the latest when left out. - `GET` [/v1/deployments/{deployment_id}/test-runs](https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-test-runs.md): Past test runs. - `GET` [/v1/test-runs/{test_run_id}](https://vestra.ai/developers/reference/api/tests/get-v1-test-runs-test-run-id.md): Pass or fail per scenario, the conversation and the judge's reason. ## [Computer setup](https://vestra.ai/developers/reference/api/computer-setup.md) Wake a user's computer before they need it. - `POST` [/v1/users/{user_id}/warm](https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-warm.md): Wake the user's computer now (for example when they open your app), so their next task starts without waiting. - `POST` [/v1/users/{user_id}/browser/fetch](https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-browser-fetch.md): Open one page in a background tab of the user's own browser, with no model, and get back the bodies of the page's own data calls that match capture. - `GET` [/v1/users/{user_id}/browser/cookies](https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-cookies.md): Which cookies of some names the user's browser holds for one site, and until when: names, sites and expiry only, never a value. - `GET` [/v1/users/{user_id}/browser/watch](https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-watch.md): Watch the browser fetch's tabs live, as text/event-stream. ## [Routines](https://vestra.ai/developers/reference/api/routines.md) Jobs Bash runs on a schedule in plain words, or whenever your system calls. - `POST` [/v1/routines](https://vestra.ai/developers/reference/api/routines/post-v1-routines.md): Create one: what to ask, and when: every (plain words such as "weekday at 08:00", read in the user's time zone), a cron schedule, or on "call" (your system posts to its trigger_url). - `GET` [/v1/routines](https://vestra.ai/developers/reference/api/routines/get-v1-routines.md): The routines of one user (user_id), or of the company's own Bash. - `GET` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id.md): One routine. - `PATCH` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/patch-v1-routines-routine-id.md): Change what it asks, when it runs (every, schedule or timezone), its filters, or pause and resume it. - `DELETE` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/delete-v1-routines-routine-id.md): Delete it. - `POST` [/v1/routines/{routine_id}/run](https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-run.md): Run it now. - `POST` [/v1/routines/{routine_id}/trigger](https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-trigger.md): Fire it with a payload from your system: a train delayed, a refund posted. - `GET` [/v1/routines/{routine_id}/runs](https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id-runs.md): Past runs and the tasks they started. ## [Batches](https://vestra.ai/developers/reference/api/batches.md) Many users at once: one ask each, followed together. - `POST` [/v1/batches](https://vestra.ai/developers/reference/api/batches/post-v1-batches.md): Start a batch: a deployment, a list of items (user_id optional, message, context, metadata) and a concurrency. - `GET` [/v1/batches/{batch_id}](https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id.md): Progress: counts by status, spend so far. - `GET` [/v1/batches/{batch_id}/items](https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id-items.md): Each item with its task id and result. - `POST` [/v1/batches/{batch_id}/cancel](https://vestra.ai/developers/reference/api/batches/post-v1-batches-batch-id-cancel.md): Stop starting new items; running ones finish. ## [Webhooks and events](https://vestra.ai/developers/reference/api/webhooks-and-events.md) Where events go, how each delivery went, and the record of every event. - `POST` [/v1/webhooks](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks.md): Register a URL and the events it wants. - `GET` [/v1/webhooks](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks.md): Your endpoints. - `GET` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id.md): One endpoint. - `PATCH` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/patch-v1-webhooks-endpoint-id.md): Change URL or events, switch it on or off. - `DELETE` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/delete-v1-webhooks-endpoint-id.md): Remove it. - `POST` [/v1/webhooks/{endpoint_id}/roll-secret](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-roll-secret.md): New signing secret; the old one is accepted for 24 hours. - `POST` [/v1/webhooks/{endpoint_id}/test](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-test.md): Send a sample event now. - `GET` [/v1/webhooks/{endpoint_id}/deliveries](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id-deliveries.md): Every attempt with status code and timing. - `POST` [/v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-deliveries-delivery-id-retry.md): Send one again. - `GET` [/v1/events](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events.md): The same events as a list you can pull, for anything you missed. - `GET` [/v1/events/{event_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events-event-id.md): One event. ## [Sessions](https://vestra.ai/developers/reference/api/sessions.md) Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. - `POST` [/v1/sessions](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions.md): Open a session for one of your users (user_id), or for the company's own Bash: chat, a screen to watch or drive, a voice call, and what Bash remembers (memory, when your company turned the panel on). - `GET` [/v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id.md): Whether a session is still open, and what it was opened with. - `DELETE` [/v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/delete-v1-sessions-session-id.md): End a session now: its token stops working, its streams close and its call hangs up. - `GET` [/v1/sessions/{session_id}/frame-policy](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-frame-policy.md): Which sites may show a session's page. - `GET` [/v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-screen.md): With the session's token: the stream credentials of the user's Bash browser, good for 5 minutes. - `POST` [/v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-screen.md): With the session's token, in control mode: the user takes the wheel or gives it back (action), or says they are driving (input), so Bash waits. - `GET` [/v1/users/{user_id}/screenshot](https://vestra.ai/developers/reference/api/sessions/get-v1-users-user-id-screenshot.md): The latest frame of the user's browser, as a JPEG. - `POST` [/v1/sessions/{session_id}/voice](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-voice.md): Called from the user's browser with the session's token and its WebRTC offer; answers the model's answer. ## [MCP server](https://vestra.ai/developers/reference/api/mcp-server.md) Bash as an MCP server, for your editor or your own agents. - `POST` [/v1/mcp](https://vestra.ai/developers/reference/api/mcp-server/post-v1-mcp.md): One MCP endpoint (streamable HTTP): the card's lines as tools, each with an optional user (with none, your company's own Bash). ## [A2A](https://vestra.ai/developers/reference/api/a2a.md) Each deployment as an agent other agents can call, over the A2A protocol. - `GET` [/v1/a2a/{deployment_id}/.well-known/agent-card.json](https://vestra.ai/developers/reference/api/a2a/get-v1-a2a-deployment-id-well-known-agent-card-json.md): What this deployment does and how to authenticate, for other agents to discover. - `POST` [/v1/a2a/{deployment_id}](https://vestra.ai/developers/reference/api/a2a/post-v1-a2a-deployment-id.md): A2A JSON-RPC: send a message, stream, get, list or cancel. ## [Contract](https://vestra.ai/developers/reference/api/contract.md) The contract itself, as OpenAPI. - `GET` [/v1/openapi.json](https://vestra.ai/developers/reference/api/contract/get-v1-openapi-json.md): The whole contract, machine-readable. --- url: https://vestra.ai/developers/reference/api/account.md --- # Account API > Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/me](https://vestra.ai/developers/reference/api/account/get-v1-me.md): Who is calling: the company, the key, and whether it is test or live. - `GET` [/v1/org](https://vestra.ai/developers/reference/api/account/get-v1-org.md): The company account: name, status, the wallet in billed dollars, day and month caps, alert thresholds, allowed return URLs, allowed embed origins, default deployment. - `PATCH` [/v1/org](https://vestra.ai/developers/reference/api/account/patch-v1-org.md): Change the name, allowed return URLs, allowed embed origins, default deployment or default memory mode. - `GET` [/v1/keys](https://vestra.ai/developers/reference/api/account/get-v1-keys.md): List keys. - `POST` [/v1/keys](https://vestra.ai/developers/reference/api/account/post-v1-keys.md): Create a test or live key. - `POST` [/v1/keys/{key_id}/roll](https://vestra.ai/developers/reference/api/account/post-v1-keys-key-id-roll.md): Replace a key. - `DELETE` [/v1/keys/{key_id}](https://vestra.ai/developers/reference/api/account/delete-v1-keys-key-id.md): Revoke a key immediately. - `GET` [/v1/limits](https://vestra.ai/developers/reference/api/account/get-v1-limits.md): The day and month caps for the company and per user, the alert thresholds and the rate limits. - `PATCH` [/v1/limits](https://vestra.ai/developers/reference/api/account/patch-v1-limits.md): Set the day and month caps for the company and per user, and the alert thresholds, within what Vestra allows. - `GET` [/v1/usage](https://vestra.ai/developers/reference/api/account/get-v1-usage.md): Exact billed spend and real task counts, by day, user, deployment, conversation, task, model or kind of spend, filtered by user, deployment or conversation. - `GET` [/v1/usage/line-items](https://vestra.ai/developers/reference/api/account/get-v1-usage-line-items.md): The charges behind one task or one conversation, newest first: the receipt you can show your user. - `GET` [/v1/audit-log](https://vestra.ai/developers/reference/api/account/get-v1-audit-log.md): Every key use, settings change and approval answer, newest first. - `GET` [/v1/models](https://vestra.ai/developers/reference/api/account/get-v1-models.md): The models a deployment may choose from. - `GET` [/v1/health](https://vestra.ai/developers/reference/api/account/get-v1-health.md): Is the API up. --- url: https://vestra.ai/developers/reference/api/account/get-v1-me.md --- # GET /v1/me > Who is calling: the company, the key, and whether it is test or live. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Who is calling: the company, the key, and whether it is test or live. Your API key, any scope. On the card: [`keys`](https://vestra.ai/developers/keys.md). Every call here is a checked sample. ## Request Nothing to send but the route. ## Answers 200 - `object` (`"me"`): Always `me`. - `org` (`object`): The company account the call acts for. - `id` (`string`): Your company account's id. Example: `org_2f8k`. - `name` (`string`): Your company's name. - `status` (`string`): `active` while the account works. Any other value means it is switched off, and calls answer 403 `org_disabled`. - `mode` (`"live" | "test"`): `live` or `test`: the mode of the key (or console session) in use. A test key sees only test users, test deployments and test money. - `key` (`object | null`): The API key in use; null when the call comes from a console session. - `id` (`string`): The key's id. Example: `key_8f2k`. - `name` (`string | null`): The name you gave the key; null when it has none. - `last4` (`string`): The last four characters of the key's secret, to tell keys apart. - `member` (`object | null`): Who is signed in to the console; null when the call uses an API key. - `role` (`"owner" | "admin" | "developer"`): Your role in the company: `owner`, `admin` or `developer`. ## Code **TypeScript** ```ts const me = await vestra.keys.me(); ``` **Python** ```python me = vestra.keys.me() ``` **CLI** ```bash vestra login ``` **HTTP** ```bash # Check your key. See your company, this key, and whether it is test or live. curl "$VESTRA_BASE_URL/v1/me" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Create a key for CI. This one can only set things up. Its secret is shown only once. curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Roll a key. You get a new secret, and the old one keeps working for an hour. curl "$VESTRA_BASE_URL/v1/keys/key_3vd/roll" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "grace_seconds": 3600 }' # Revoke a key. It stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/keys/key_3vd" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/get-v1-org.md --- # GET /v1/org > The company account: name, status, the wallet in billed dollars, day and month caps, alert thresholds, allowed return URLs, allowed embed origins, default deployment. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. The company account: name, status, the wallet in billed dollars, day and month caps, alert thresholds, allowed return URLs, allowed embed origins, default deployment. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Over HTTP it is made from the contract: fill in every value. ## Request Nothing to send but the route. ## Answers 200 - `id` (`string`): The company account's id. Example: `org_2f8k`. - `object` (`"org"`): Always `org`. - `name` (`string`): The company's name. - `status` (`string`): `active` while the account works. Any other value means it is switched off, and calls answer 403 `org_disabled`. - `mode` (`"live" | "test"`): The mode these settings and this wallet belong to: `live` or `test`, the mode of the key in use. - `allowed_return_urls` (`string[]`): Where a connect link may send a person back to (`return_url`). Empty: no return URL is allowed. - `allowed_embed_origins` (`string[]`): The https origins whose pages may show Bash in an iframe, and open a voice call. - `default_deployment` (`string | null`): The deployment a conversation runs when neither it nor its user names one; null: Bash as it comes, with no deployment of yours. - `require_user` (`boolean`): When true, a request that names no `user_id` answers 400 `user_required` instead of reaching the company's own Bash. - `memory_panel` (`boolean`): Whether a session may show the user what Bash remembers (POST /v1/sessions with `memory: true`). Off until you turn it on. - `memory_mode` (`"vestra" | "customer" | "none"`): Who keeps what Bash remembers about each user, unless a deployment says otherwise: `vestra` (Bash keeps it), `customer` (your side keeps it, at `memory_endpoint`) or `none`. - `memory_endpoint` (`string | null`): With `memory_mode: customer`, the https URL Bash asks for what to remember about a user; null otherwise. - `otlp_endpoint` (`string | null`): Where Bash sends its traces, as OpenTelemetry (OTLP over HTTP); null: none are sent. - `otlp_headers_secret` (`string | null`): The name of your secret that holds the headers sent with each trace; its value is never returned. Null: none. - `balance_usd` (`number`): This mode's wallet, in billed US dollars, up to 5 decimals. Work stops with 402 `wallet_empty` when it runs out. - `caps` (`object`): This mode's spending caps, for the company and per user, per day and per month. - `org_daily_usd` (`number | null`): The company's spending cap per day (UTC), in US dollars, across all its users; null: none, up to the account's ceiling. Limits: 0 to 1000000. - `user_daily_usd` (`number | null`): The default cap per user per day (UTC), in US dollars; a user's own `spend_cap_usd` wins. Null: none. Limits: 0 to 1000000. - `max_org_daily_usd` (`number`): The highest the company's daily cap may be set, in US dollars. Ask Vestra to raise it. Limits: 0 to 1000000. - `org_monthly_usd` (`number | null`): The company's cap per calendar month (UTC), in US dollars. Null (the default): none. Limits: 0 to 1000000. - `user_monthly_usd` (`number | null`): The default cap per user per calendar month (UTC), in US dollars; a user's own `monthly_cap_usd` wins. Null (the default): none. Limits: 0 to 1000000. - `usage_alerts` (`object`): When Bash tells your webhooks that spend is nearing a cap. - `thresholds` (`integer[]`): Percents of a cap at which `usage.threshold_reached` fires; reaching 100 always fires `usage.cap_reached`. Limits: at most 5 items. - `created_at` (`string`): When the account was made, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/org" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/patch-v1-org.md --- # PATCH /v1/org > Change the name, allowed return URLs, allowed embed origins, default deployment or default memory mode. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Change the name, allowed return URLs, allowed embed origins, default deployment or default memory mode. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Every call here is a checked sample. ## Request Body: - `name` (`string`): The company's name, 1 to 200 characters. Limits: 1 to 200 characters. - `allowed_return_urls` (`(string (URL))[]`): Where a connect link may send a person back to; replaces the whole list. At most 20. Limits: at most 20 items. Example: `["https://app.example.com/connected"]`. - `allowed_embed_origins` (`(string (URL))[]`): The https origins whose pages may show Bash in an iframe; replaces the whole list. At most 20. Limits: at most 20 items. Example: `["https://app.example.com"]`. - `default_deployment` (`string | null`): The deployment a conversation runs when it names none, by name or `dep_` id; null clears it. Limits: at least 1 character. - `require_user` (`boolean`): True: every request must name a `user_id`, and none reaches the company's own Bash. - `memory_panel` (`boolean`): True: a session may show the user what Bash remembers. - `memory_mode` (`"vestra" | "customer" | "none"`): Who keeps what Bash remembers: `vestra`, `customer` (send `memory_endpoint` too) or `none`. - `memory_endpoint` (`string (URL) | null`): With `memory_mode: customer`, the https URL Bash asks for what to remember; null clears it. - `otlp_endpoint` (`string (URL) | null`): Where to send traces, as OpenTelemetry over HTTP; null stops them. - `otlp_headers_secret` (`string | null`): The name of one of your secrets (PUT /v1/secrets/{name}) whose value is a JSON object of header names to values, sent with each trace; null clears it. Example: `OTLP_HEADERS`. ## Answers 200 - `id` (`string`): The company account's id. Example: `org_2f8k`. - `object` (`"org"`): Always `org`. - `name` (`string`): The company's name. - `status` (`string`): `active` while the account works. Any other value means it is switched off, and calls answer 403 `org_disabled`. - `mode` (`"live" | "test"`): The mode these settings and this wallet belong to: `live` or `test`, the mode of the key in use. - `allowed_return_urls` (`string[]`): Where a connect link may send a person back to (`return_url`). Empty: no return URL is allowed. - `allowed_embed_origins` (`string[]`): The https origins whose pages may show Bash in an iframe, and open a voice call. - `default_deployment` (`string | null`): The deployment a conversation runs when neither it nor its user names one; null: Bash as it comes, with no deployment of yours. - `require_user` (`boolean`): When true, a request that names no `user_id` answers 400 `user_required` instead of reaching the company's own Bash. - `memory_panel` (`boolean`): Whether a session may show the user what Bash remembers (POST /v1/sessions with `memory: true`). Off until you turn it on. - `memory_mode` (`"vestra" | "customer" | "none"`): Who keeps what Bash remembers about each user, unless a deployment says otherwise: `vestra` (Bash keeps it), `customer` (your side keeps it, at `memory_endpoint`) or `none`. - `memory_endpoint` (`string | null`): With `memory_mode: customer`, the https URL Bash asks for what to remember about a user; null otherwise. - `otlp_endpoint` (`string | null`): Where Bash sends its traces, as OpenTelemetry (OTLP over HTTP); null: none are sent. - `otlp_headers_secret` (`string | null`): The name of your secret that holds the headers sent with each trace; its value is never returned. Null: none. - `balance_usd` (`number`): This mode's wallet, in billed US dollars, up to 5 decimals. Work stops with 402 `wallet_empty` when it runs out. - `caps` (`object`): This mode's spending caps, for the company and per user, per day and per month. - `org_daily_usd` (`number | null`): The company's spending cap per day (UTC), in US dollars, across all its users; null: none, up to the account's ceiling. Limits: 0 to 1000000. - `user_daily_usd` (`number | null`): The default cap per user per day (UTC), in US dollars; a user's own `spend_cap_usd` wins. Null: none. Limits: 0 to 1000000. - `max_org_daily_usd` (`number`): The highest the company's daily cap may be set, in US dollars. Ask Vestra to raise it. Limits: 0 to 1000000. - `org_monthly_usd` (`number | null`): The company's cap per calendar month (UTC), in US dollars. Null (the default): none. Limits: 0 to 1000000. - `user_monthly_usd` (`number | null`): The default cap per user per calendar month (UTC), in US dollars; a user's own `monthly_cap_usd` wins. Null (the default): none. Limits: 0 to 1000000. - `usage_alerts` (`object`): When Bash tells your webhooks that spend is nearing a cap. - `thresholds` (`integer[]`): Percents of a cap at which `usage.threshold_reached` fires; reaching 100 always fires `usage.cap_reached`. Limits: at most 5 items. - `created_at` (`string`): When the account was made, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash # Describe the job, naming the deployment. Its questions arrive on the conversation the answer names: reply there, as the init line shows. # A slug already taken (409 slug_taken, say by an init you stopped)? Send another, such as travel-desk-2. curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Travel desk", "slug": "travel-desk", "job": "Answer questions from my travellers about their bookings. Look bookings up with get_booking." }' # The interview saves the draft in test mode. Play its starter tests. curl -X POST "$VESTRA_BASE_URL/v1/deployments/travel-desk/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Make it your default deployment: every ask below that names no deployment reaches it. This replaces any default you had. curl -X PATCH "$VESTRA_BASE_URL/v1/org" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "default_deployment": "travel-desk" }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/get-v1-keys.md --- # GET /v1/keys > List keys. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. List keys. Only the last four characters are ever shown again. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Over HTTP it is made from the contract: fill in every value. ## Request Nothing to send but the route. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The key's id. Example: `key_8f2k`. - `object` (`"api_key"`): Always `api_key`. - `mode` (`"live" | "test"`): `live` or `test`: what the key reaches. A test key sees only test users, test deployments and test money. - `name` (`string | null`): The name you gave it; null when it has none. - `prefix` (`string`): The start of the secret, to tell keys apart: `vk_live_` or `vk_test_` and a few characters. Example: `vk_test_8f2k`. - `last4` (`string`): The last four characters of the secret. - `scopes` (`("all" | "runtime" | "setup" | "admin")[]`): What the key may call: `all`, or any of `runtime` (conversations, work, routines, connected apps, files, memory, usage), `setup` (deployments, tests, secrets) and `admin` (users and keys). - `status` (`"active" | "expiring" | "expired" | "revoked"`): `active`: works, with no end. `expiring`: works until `expires_at` (a rolled key in its grace period, or one made to expire). `expired` or `revoked`: refused. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `last_used_at` (`string | null`): When it last made a call; null when it never has. - `expires_at` (`string | null`): When it stops working; null when it never does. - `revoked_at` (`string | null`): When it was revoked; null while it is not. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const keys = await vestra.keys.list(); ``` **Python** ```python keys = vestra.keys.list() ``` **CLI** ```bash vestra keys ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/post-v1-keys.md --- # POST /v1/keys > Create a test or live key. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Create a test or live key. The full key is shown once. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Every call here is a checked sample. ## Request Body, if you send one: - `name` (`string`): A name for the key, to tell it apart later, up to 100 characters. Limits: 1 to 100 characters. Example: `production`. - `scopes` (`("all" | "runtime" | "setup" | "admin")[]`): What it may call: `all` alone, or 1 to 3 of `runtime`, `setup` and `admin`. Absent: `all`. A key only gives the scopes it has. Limits: 1 to 3 items. Example: `["runtime"]`. - `expires_at` (`string (date and time)`): When the key stops working, an ISO 8601 date and time in the future. Absent: never. ## Answers 201 - `id` (`string`): The key's id. Example: `key_8f2k`. - `object` (`"api_key"`): Always `api_key`. - `mode` (`"live" | "test"`): `live` or `test`: what the key reaches. A test key sees only test users, test deployments and test money. - `name` (`string | null`): The name you gave it; null when it has none. - `prefix` (`string`): The start of the secret, to tell keys apart: `vk_live_` or `vk_test_` and a few characters. Example: `vk_test_8f2k`. - `last4` (`string`): The last four characters of the secret. - `scopes` (`("all" | "runtime" | "setup" | "admin")[]`): What the key may call: `all`, or any of `runtime` (conversations, work, routines, connected apps, files, memory, usage), `setup` (deployments, tests, secrets) and `admin` (users and keys). - `status` (`"active" | "expiring" | "expired" | "revoked"`): `active`: works, with no end. `expiring`: works until `expires_at` (a rolled key in its grace period, or one made to expire). `expired` or `revoked`: refused. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `last_used_at` (`string | null`): When it last made a call; null when it never has. - `expires_at` (`string | null`): When it stops working; null when it never does. - `revoked_at` (`string | null`): When it was revoked; null while it is not. - `secret` (`string`): The whole key. It is shown this once: store it now, as Vestra keeps only its hash. Example: `vk_test_...`. ## Code **TypeScript** ```ts const key = await vestra.keys.create(); ``` **Python** ```python key = vestra.keys.create() ``` **CLI** ```bash vestra keys create ``` **HTTP** ```bash # Check your key. See your company, this key, and whether it is test or live. curl "$VESTRA_BASE_URL/v1/me" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Create a key for CI. This one can only set things up. Its secret is shown only once. curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Roll a key. You get a new secret, and the old one keeps working for an hour. curl "$VESTRA_BASE_URL/v1/keys/key_3vd/roll" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "grace_seconds": 3600 }' # Revoke a key. It stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/keys/key_3vd" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/post-v1-keys-key-id-roll.md --- # POST /v1/keys/{key_id}/roll > Replace a key. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Replace a key. The old one keeps working for a grace period you choose, then stops. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Every call here is a checked sample. ## Request Path: - `key_id` (`string`, required): The API key's id (`key_...`). Body, if you send one: - `grace_seconds` (`integer`): How long the old key keeps working, in seconds, up to 7 days. Default 86400 (a day); 0 stops it at once. Default: `86400`. Limits: 0 to 604800. ## Answers 201 - `object` (`"api_key_roll"`): Always `api_key_roll`. - `key` (`object`): The new key, with its secret, shown once. - `id` (`string`): The key's id. Example: `key_8f2k`. - `object` (`"api_key"`): Always `api_key`. - `mode` (`"live" | "test"`): `live` or `test`: what the key reaches. A test key sees only test users, test deployments and test money. - `name` (`string | null`): The name you gave it; null when it has none. - `prefix` (`string`): The start of the secret, to tell keys apart: `vk_live_` or `vk_test_` and a few characters. Example: `vk_test_8f2k`. - `last4` (`string`): The last four characters of the secret. - `scopes` (`("all" | "runtime" | "setup" | "admin")[]`): What the key may call: `all`, or any of `runtime` (conversations, work, routines, connected apps, files, memory, usage), `setup` (deployments, tests, secrets) and `admin` (users and keys). - `status` (`"active" | "expiring" | "expired" | "revoked"`): `active`: works, with no end. `expiring`: works until `expires_at` (a rolled key in its grace period, or one made to expire). `expired` or `revoked`: refused. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `last_used_at` (`string | null`): When it last made a call; null when it never has. - `expires_at` (`string | null`): When it stops working; null when it never does. - `revoked_at` (`string | null`): When it was revoked; null while it is not. - `secret` (`string`): The whole key. It is shown this once: store it now, as Vestra keeps only its hash. Example: `vk_test_...`. - `previous` (`object`): The old key: it works until its `expires_at`, then stops. - `id` (`string`): The key's id. Example: `key_8f2k`. - `object` (`"api_key"`): Always `api_key`. - `mode` (`"live" | "test"`): `live` or `test`: what the key reaches. A test key sees only test users, test deployments and test money. - `name` (`string | null`): The name you gave it; null when it has none. - `prefix` (`string`): The start of the secret, to tell keys apart: `vk_live_` or `vk_test_` and a few characters. Example: `vk_test_8f2k`. - `last4` (`string`): The last four characters of the secret. - `scopes` (`("all" | "runtime" | "setup" | "admin")[]`): What the key may call: `all`, or any of `runtime` (conversations, work, routines, connected apps, files, memory, usage), `setup` (deployments, tests, secrets) and `admin` (users and keys). - `status` (`"active" | "expiring" | "expired" | "revoked"`): `active`: works, with no end. `expiring`: works until `expires_at` (a rolled key in its grace period, or one made to expire). `expired` or `revoked`: refused. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `last_used_at` (`string | null`): When it last made a call; null when it never has. - `expires_at` (`string | null`): When it stops working; null when it never does. - `revoked_at` (`string | null`): When it was revoked; null while it is not. ## Code **TypeScript** ```ts const rolled = await vestra.keys.roll('key_8sd'); ``` **Python** ```python rolled = vestra.keys.roll("key_8sd") ``` **CLI** ```bash vestra keys roll key_8sd ``` **HTTP** ```bash # Check your key. See your company, this key, and whether it is test or live. curl "$VESTRA_BASE_URL/v1/me" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Create a key for CI. This one can only set things up. Its secret is shown only once. curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Roll a key. You get a new secret, and the old one keeps working for an hour. curl "$VESTRA_BASE_URL/v1/keys/key_3vd/roll" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "grace_seconds": 3600 }' # Revoke a key. It stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/keys/key_3vd" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/delete-v1-keys-key-id.md --- # DELETE /v1/keys/{key_id} > Revoke a key immediately. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Revoke a key immediately. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Every call here is a checked sample. ## Request Path: - `key_id` (`string`, required): The API key's id (`key_...`). ## Answers 200 - `id` (`string`): The key's id. Example: `key_8f2k`. - `object` (`"api_key"`): Always `api_key`. - `mode` (`"live" | "test"`): `live` or `test`: what the key reaches. A test key sees only test users, test deployments and test money. - `name` (`string | null`): The name you gave it; null when it has none. - `prefix` (`string`): The start of the secret, to tell keys apart: `vk_live_` or `vk_test_` and a few characters. Example: `vk_test_8f2k`. - `last4` (`string`): The last four characters of the secret. - `scopes` (`("all" | "runtime" | "setup" | "admin")[]`): What the key may call: `all`, or any of `runtime` (conversations, work, routines, connected apps, files, memory, usage), `setup` (deployments, tests, secrets) and `admin` (users and keys). - `status` (`"active" | "expiring" | "expired" | "revoked"`): `active`: works, with no end. `expiring`: works until `expires_at` (a rolled key in its grace period, or one made to expire). `expired` or `revoked`: refused. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `last_used_at` (`string | null`): When it last made a call; null when it never has. - `expires_at` (`string | null`): When it stops working; null when it never does. - `revoked_at` (`string | null`): When it was revoked; null while it is not. ## Code **TypeScript** ```ts await vestra.keys.revoke('key_8sd'); ``` **Python** ```python vestra.keys.revoke("key_8sd") ``` **CLI** ```bash vestra keys revoke key_8sd ``` **HTTP** ```bash # Check your key. See your company, this key, and whether it is test or live. curl "$VESTRA_BASE_URL/v1/me" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Create a key for CI. This one can only set things up. Its secret is shown only once. curl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Roll a key. You get a new secret, and the old one keeps working for an hour. curl "$VESTRA_BASE_URL/v1/keys/key_3vd/roll" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "grace_seconds": 3600 }' # Revoke a key. It stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/keys/key_3vd" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/get-v1-limits.md --- # GET /v1/limits > The day and month caps for the company and per user, the alert thresholds and the rate limits. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. The day and month caps for the company and per user, the alert thresholds and the rate limits. Your API key, scope `runtime` or `all`. On the card: [`usage`](https://vestra.ai/developers/usage.md). Over HTTP it is made from the contract: fill in every value. ## Request Nothing to send but the route. ## Answers 200 - `object` (`"limits"`): Always `limits`. - `mode` (`"live" | "test"`): The mode these limits belong to: the mode of the key in use. - `org_daily_usd` (`number | null`): The company's spending cap per day (UTC), in US dollars, across all its users; null: none, up to the account's ceiling. Limits: 0 to 1000000. - `user_daily_usd` (`number | null`): The default cap per user per day (UTC), in US dollars; a user's own `spend_cap_usd` wins. Null: none. Limits: 0 to 1000000. - `max_org_daily_usd` (`number`): The highest the company's daily cap may be set, in US dollars. Ask Vestra to raise it. Limits: 0 to 1000000. - `org_monthly_usd` (`number | null`): The company's cap per calendar month (UTC), in US dollars. Null (the default): none. Limits: 0 to 1000000. - `user_monthly_usd` (`number | null`): The default cap per user per calendar month (UTC), in US dollars; a user's own `monthly_cap_usd` wins. Null (the default): none. Limits: 0 to 1000000. - `alert_thresholds` (`integer[]`): Percents of a cap at which usage alerts fire, for the company and each user, per day and per month. Limits: at most 5 items. - `rate_limits` (`object`): How fast you may call, fixed per account. - `requests_per_minute` (`integer`): Calls a key may make per minute before 429 `rate_limited`. - `concurrent_task_starts` (`integer`): How many computers your company may be waking at once with `warm`; past it, `warm` answers 429 `warm_limit`. Starting work is not limited by this number yet. ## Code **TypeScript** ```ts const limits = await vestra.usage.limits(); ``` **Python** ```python limits = vestra.usage.limits() ``` **CLI** ```bash vestra usage caps ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/patch-v1-limits.md --- # PATCH /v1/limits > Set the day and month caps for the company and per user, and the alert thresholds, within what Vestra allows. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Set the day and month caps for the company and per user, and the alert thresholds, within what Vestra allows. Your API key, scope `admin` or `all`. On the card: [`usage`](https://vestra.ai/developers/usage.md). Every call here is a checked sample. ## Request Body: - `org_daily_usd` (`number | null`): The company's cap per day (UTC), in US dollars with at most 2 decimals, up to `max_org_daily_usd`; null removes it. Limits: 0 to 1000000. - `user_daily_usd` (`number | null`): The default cap per user per day (UTC), in US dollars, at most the company cap; null removes it. Limits: 0 to 1000000. - `org_monthly_usd` (`number | null`): The company's cap per calendar month (UTC), in US dollars; null removes it. Limits: 0 to 1000000. - `user_monthly_usd` (`number | null`): The default cap per user per calendar month (UTC), at most the company monthly cap; null removes it. Limits: 0 to 1000000. - `alert_thresholds` (`integer[]`): Percents of a cap at which `usage.threshold_reached` fires, whole numbers from 1 to 100, at most four below 100. 100 always fires `usage.cap_reached`. Limits: at most 5 items. Example: `[50,80]`. ## Answers 200 - `object` (`"limits"`): Always `limits`. - `mode` (`"live" | "test"`): The mode these limits belong to: the mode of the key in use. - `org_daily_usd` (`number | null`): The company's spending cap per day (UTC), in US dollars, across all its users; null: none, up to the account's ceiling. Limits: 0 to 1000000. - `user_daily_usd` (`number | null`): The default cap per user per day (UTC), in US dollars; a user's own `spend_cap_usd` wins. Null: none. Limits: 0 to 1000000. - `max_org_daily_usd` (`number`): The highest the company's daily cap may be set, in US dollars. Ask Vestra to raise it. Limits: 0 to 1000000. - `org_monthly_usd` (`number | null`): The company's cap per calendar month (UTC), in US dollars. Null (the default): none. Limits: 0 to 1000000. - `user_monthly_usd` (`number | null`): The default cap per user per calendar month (UTC), in US dollars; a user's own `monthly_cap_usd` wins. Null (the default): none. Limits: 0 to 1000000. - `alert_thresholds` (`integer[]`): Percents of a cap at which usage alerts fire, for the company and each user, per day and per month. Limits: at most 5 items. - `rate_limits` (`object`): How fast you may call, fixed per account. - `requests_per_minute` (`integer`): Calls a key may make per minute before 429 `rate_limited`. - `concurrent_task_starts` (`integer`): How many computers your company may be waking at once with `warm`; past it, `warm` answers 429 `warm_limit`. Starting work is not limited by this number yet. ## Code **TypeScript** ```ts await vestra.usage.caps({ user_monthly_usd: 20 }); ``` **Python** ```python vestra.usage.caps(user_monthly_usd=20) ``` **CLI** ```bash vestra usage caps --customer-monthly 20 ``` **HTTP** ```bash # See costs by user. This is what everything cost since the 1st, in billed US dollars. curl "$VESTRA_BASE_URL/v1/usage?from=2026-10-01&group_by=user" \ -H "Authorization: Bearer $VESTRA_API_KEY" # See one user's spend. curl "$VESTRA_BASE_URL/v1/usage?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get a conversation's receipt. It lists every charge, newest first. curl "$VESTRA_BASE_URL/v1/usage/line-items?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_monthly_usd": 20, "alert_thresholds": [50, 80] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "usage", "arguments": { "from": "2026-10-01", "group_by": "user" } } } ``` --- url: https://vestra.ai/developers/reference/api/account/get-v1-usage.md --- # GET /v1/usage > Exact billed spend and real task counts, by day, user, deployment, conversation, task, model or kind of spend, filtered by user, deployment or conversation. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Exact billed spend and real task counts, by day, user, deployment, conversation, task, model or kind of spend, filtered by user, deployment or conversation. An erased user's spend stays in the totals, as the user unknown. Your API key, scope `runtime` or `all`. On the card: [`usage`](https://vestra.ai/developers/usage.md). Every call here is a checked sample. ## Request Query: - `from` (`string`): The first day, inclusive, as YYYY-MM-DD (UTC). Absent: 29 days before `to`. - `to` (`string`): The last day, inclusive, as YYYY-MM-DD (UTC). At most 31 days from `from` to `to`, counting both, so `to` is at most 30 days after `from`. Absent: today. - `group_by` (`"day" | "user" | "deployment" | "conversation" | "task" | "model" | "category"`): How to split the spend: by `day` (the default), `user`, `deployment`, `conversation`, `task`, `model` or `category`. - `user_id` (`string`): Only this user's spend, by your own id for them. Absent: every user's, and the company's own Bash's. - `deployment_id` (`string`): Only this deployment's spend, by its `dep_` id. - `conversation_id` (`string`): Only this conversation's spend, by its `conv_` id. ## Answers 200 - `object` (`"usage"`): Always `usage`. - `mode` (`"live" | "test"`): The mode this spend is from: the mode of the key in use. - `from` (`string`): The first day counted, as YYYY-MM-DD (UTC). - `to` (`string`): The last day counted, as YYYY-MM-DD (UTC). - `group_by` (`"day" | "user" | "deployment" | "conversation" | "task" | "model" | "category"`): How the rows are split. - `data` (`object[]`): One row per group, with what it spent. - `key` (`string`): The group, as `group_by` says: the day (`2026-09-30`), your user id, the deployment's, conversation's or task's id, the model, or the category. `none` when the spend belongs to no such thing; `unknown` for spend with no user. - `spend_usd` (`number`): Billed US dollars, exact to 5 decimals. A group whose refunds exceed its charges reads 0. - `tasks` (`integer`): The pieces of work that spent in this group. Bash's own replies are not counted as work. - `total` (`object`): The rows added up. - `spend_usd` (`number`): Billed US dollars across every row. - `tasks` (`integer`): Pieces of work across every row. ## Code **TypeScript** ```ts const usage = await vestra.usage(); ``` **Python** ```python usage = vestra.usage() ``` **CLI** ```bash vestra usage ``` **HTTP** ```bash # See costs by user. This is what everything cost since the 1st, in billed US dollars. curl "$VESTRA_BASE_URL/v1/usage?from=2026-10-01&group_by=user" \ -H "Authorization: Bearer $VESTRA_API_KEY" # See one user's spend. curl "$VESTRA_BASE_URL/v1/usage?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get a conversation's receipt. It lists every charge, newest first. curl "$VESTRA_BASE_URL/v1/usage/line-items?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_monthly_usd": 20, "alert_thresholds": [50, 80] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "usage", "arguments": { "from": "2026-10-01", "group_by": "user" } } } ``` --- url: https://vestra.ai/developers/reference/api/account/get-v1-usage-line-items.md --- # GET /v1/usage/line-items > The charges behind one task or one conversation, newest first: the receipt you can show your user. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. The charges behind one task or one conversation, newest first: the receipt you can show your user. Your API key, scope `runtime` or `all`. On the card: [`usage`](https://vestra.ai/developers/usage.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `task_id` (`string`): The charges behind this piece of work, by its `task_` id. Send this or `conversation_id`. - `conversation_id` (`string`): The charges behind this conversation, by its `conv_` id. Send this or `task_id`. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The charge's id. - `object` (`"usage.line_item"`): Always `usage.line_item`. - `category` (`"orchestrator" | "intelligence" | "tools" | "compute" | "background" | "voice"`): The kind of spend: `orchestrator` (Bash's replies), `intelligence` (its thinking at work), `tools`, `compute`, `background` or `voice`. - `amount_usd` (`number`): Billed US dollars; a refund is negative. A subject's items add up to its `cost_usd`. - `model` (`string | null`): The model the charge was for; null when it was for no model. - `description` (`string`): What the charge was, in a few fixed words such as `Work by Bash`; a refund starts `Refund: `. - `created_at` (`string`): When it was charged, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const receipt = await vestra.usage.lineItems({ conversation: 'conv_8f2k' }); ``` **Python** ```python receipt = vestra.usage.line_items(conversation="conv_8f2k") ``` **CLI** ```bash vestra usage items conv_8f2k ``` **HTTP** ```bash # See costs by user. This is what everything cost since the 1st, in billed US dollars. curl "$VESTRA_BASE_URL/v1/usage?from=2026-10-01&group_by=user" \ -H "Authorization: Bearer $VESTRA_API_KEY" # See one user's spend. curl "$VESTRA_BASE_URL/v1/usage?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get a conversation's receipt. It lists every charge, newest first. curl "$VESTRA_BASE_URL/v1/usage/line-items?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set caps and alerts. This sets a monthly cap for each user, with alerts at 50% and 80% of it. curl -X PATCH "$VESTRA_BASE_URL/v1/limits" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_monthly_usd": 20, "alert_thresholds": [50, 80] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "usage", "arguments": { "from": "2026-10-01", "group_by": "user" } } } ``` --- url: https://vestra.ai/developers/reference/api/account/get-v1-audit-log.md --- # GET /v1/audit-log > Every key use, settings change and approval answer, newest first. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Every key use, settings change and approval answer, newest first. Your API key, scope `admin` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The entry's id. - `object` (`"audit_event"`): Always `audit_event`. - `mode` (`"live" | "test" | null`): The mode it happened in; null for a Vestra staff action on the company account. - `action` (`string`): What was done, such as `task.deleted` or `notebook.created`. Example: `task.deleted`. - `actor` (`object`): Who did it. - `kind` (`string`): Who did it: `key` (an API key), `console` (a person signed in), `client` (a session token) or `staff` (Vestra). - `id` (`string | null`): The key's id, the signed-in person's, or the session's; null when there is none to show. - `target` (`object`): What it was done to. - `type` (`string`): What kind of thing it was done to, such as `task`, `notebook` or `interaction`. - `id` (`string`): Its id. - `detail` (`map of any | null`): More about it, such as the fields that changed. It never holds a secret or a key; null when there is nothing more. - `created_at` (`string`): When it happened, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/audit-log" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/get-v1-models.md --- # GET /v1/models > The models a deployment may choose from. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. The models a deployment may choose from. Your API key, scope `runtime` or `all`. On the card: [`keys`](https://vestra.ai/developers/keys.md). Over HTTP it is made from the contract: fill in every value. ## Request Nothing to send but the route. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The model's id: what a deployment's `model` takes. - `object` (`"model"`): Always `model`. - `label` (`string`): Its name, as a person reads it. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/models" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/account/get-v1-health.md --- # GET /v1/health > Is the API up. In [Account API](https://vestra.ai/developers/reference/api/account.md): Your company's account: who is calling, its keys, its caps, what it spent and the models it may use. Send `Authorization: Bearer vk_test_...` on every call. Is the API up. No key needed. No key: anyone may call it. On the card: [`keys`](https://vestra.ai/developers/keys.md). Every call here is a checked sample. ## Request Nothing to send but the route. ## Answers 200 - `object` (`"health"`): Always `health`. - `ok` (`true`): Always true: the API is up and answering. - `time` (`string`): The server's time, as an ISO 8601 date and time in UTC. Example: `2026-10-02T08:30:00.000Z`. ## Code **HTTP** ```bash # Nothing to install. Is the API up? No key needed. curl "$VESTRA_BASE_URL/v1/health" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/users.md --- # Users API > Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/users](https://vestra.ai/developers/reference/api/users/post-v1-users.md): Create one of your users, or return the existing one. - `GET` [/v1/users](https://vestra.ai/developers/reference/api/users/get-v1-users.md): List your users. - `GET` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/get-v1-users-user-id.md): One user: status, profile, memory mode, default deployment, and what they spent today and this month against their caps. - `PATCH` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/patch-v1-users-user-id.md): Change name, language, time zone, metadata, day or month spend cap, default deployment, or status (active or disabled). - `DELETE` [/v1/users/{user_id}](https://vestra.ai/developers/reference/api/users/delete-v1-users-user-id.md): Erase the user and everything Bash holds for them: computer, files, memory. - `POST` [/v1/users/{user_id}/export](https://vestra.ai/developers/reference/api/users/post-v1-users-user-id-export.md): Package everything held for the user into one download, for data-rights requests. --- url: https://vestra.ai/developers/reference/api/users/post-v1-users.md --- # POST /v1/users > Create one of your users, or return the existing one. In [Users API](https://vestra.ai/developers/reference/api/users.md): Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. Create one of your users, or return the existing one. Safe to repeat with the same user_id. Your API key, scope `admin` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `user_id` (`string`, required): Your own id for the user. Sending one that exists changes nothing and makes no second user. Example: `traveller-42`. - `name` (`string`): The user's name, as Bash may call them: at most 200 characters. Limits: 1 to 200 characters. Example: `Priya Sharma`. - `email` (`string (email)`): The user's email address. Limits: at most 320 characters. Example: `priya@example.com`. - `locale` (`string`): The user's language and region, as a BCP 47 tag. Limits: 2 to 35 characters. Example: `en-IN`. - `timezone` (`string`): The user's time zone, as an IANA name: their routines run in it. Limits: 1 to 64 characters. Example: `Asia/Kolkata`. - `metadata` (`map of any`): Your own facts about the user, as JSON, given back as you sent them. ## Answers 200 - `id` (`string`): Your own id for the user. Example: `traveller-42`. - `object` (`"user"`): Always user. - `name` (`string | null`): The user's name; null when you gave none. - `email` (`string | null`): The user's email address; null when you gave none. - `locale` (`string | null`): The user's language and region (BCP 47); null when you gave none. - `timezone` (`string | null`): The user's time zone (IANA); null when you gave none, and their routines then run in UTC. - `metadata` (`map of any`): Your own facts about the user; empty when you gave none. - `status` (`"active" | "disabled"`): active, or disabled: a disabled user's calls are refused (403 user_disabled) until you turn them back on. - `spend_cap_usd` (`number | null`): The user's own cap per day, in billed US dollars, never above your company's; null: your company's cap for each user, if any. - `monthly_cap_usd` (`number | null`): The user's own cap per calendar month (UTC), in billed US dollars; null: your company's cap for each user, if any. - `default_deployment` (`string | null`): The deployment their conversations use when none is named (dep_...); null: your company's default. - `memory_mode` (`"vestra" | "customer" | "none"`): Where Bash's memory of this user lives: `vestra`, `customer` (your memory endpoint), or `none`. - `is_service_user` (`boolean`): true for a user Vestra keeps for itself, such as your company's own Bash; false for your users. - `created_at` (`string`): When the user was made, as an ISO 8601 time. - `spend_today_usd` (`number`): What they have spent today (UTC), in billed US dollars. Example: `1.24`. - `spend_month_usd` (`number`): What they have spent this calendar month (UTC), in billed US dollars. Example: `18.6`. - `caps_remaining` (`object`): What is left under every cap that applies to them: the smaller of their own and your company's. - `day_usd` (`number | null`): What they may still spend today, in US dollars; null when no cap applies. - `month_usd` (`number | null`): What they may still spend this month, in US dollars; null when no cap applies. ## Code **TypeScript** ```ts await vestra.users.create('traveller-42'); ``` **Python** ```python vestra.users.create("traveller-42") ``` **HTTP** ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` **CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/users/get-v1-users.md --- # GET /v1/users > List your users. In [Users API](https://vestra.ai/developers/reference/api/users.md): Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. List your users. Your API key, scope `runtime` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): Your own id for the user. Example: `traveller-42`. - `object` (`"user"`): Always user. - `name` (`string | null`): The user's name; null when you gave none. - `email` (`string | null`): The user's email address; null when you gave none. - `locale` (`string | null`): The user's language and region (BCP 47); null when you gave none. - `timezone` (`string | null`): The user's time zone (IANA); null when you gave none, and their routines then run in UTC. - `metadata` (`map of any`): Your own facts about the user; empty when you gave none. - `status` (`"active" | "disabled"`): active, or disabled: a disabled user's calls are refused (403 user_disabled) until you turn them back on. - `spend_cap_usd` (`number | null`): The user's own cap per day, in billed US dollars, never above your company's; null: your company's cap for each user, if any. - `monthly_cap_usd` (`number | null`): The user's own cap per calendar month (UTC), in billed US dollars; null: your company's cap for each user, if any. - `default_deployment` (`string | null`): The deployment their conversations use when none is named (dep_...); null: your company's default. - `memory_mode` (`"vestra" | "customer" | "none"`): Where Bash's memory of this user lives: `vestra`, `customer` (your memory endpoint), or `none`. - `is_service_user` (`boolean`): true for a user Vestra keeps for itself, such as your company's own Bash; false for your users. - `created_at` (`string`): When the user was made, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const users = await vestra.users.list(); ``` **Python** ```python users = vestra.users.list() ``` **CLI** ```bash vestra users ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/users/get-v1-users-user-id.md --- # GET /v1/users/{user_id} > One user: status, profile, memory mode, default deployment, and what they spent today and this month against their caps. In [Users API](https://vestra.ai/developers/reference/api/users.md): Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. One user: status, profile, memory mode, default deployment, and what they spent today and this month against their caps. Your API key, scope `runtime` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Every call here is a checked sample. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. ## Answers 200 - `id` (`string`): Your own id for the user. Example: `traveller-42`. - `object` (`"user"`): Always user. - `name` (`string | null`): The user's name; null when you gave none. - `email` (`string | null`): The user's email address; null when you gave none. - `locale` (`string | null`): The user's language and region (BCP 47); null when you gave none. - `timezone` (`string | null`): The user's time zone (IANA); null when you gave none, and their routines then run in UTC. - `metadata` (`map of any`): Your own facts about the user; empty when you gave none. - `status` (`"active" | "disabled"`): active, or disabled: a disabled user's calls are refused (403 user_disabled) until you turn them back on. - `spend_cap_usd` (`number | null`): The user's own cap per day, in billed US dollars, never above your company's; null: your company's cap for each user, if any. - `monthly_cap_usd` (`number | null`): The user's own cap per calendar month (UTC), in billed US dollars; null: your company's cap for each user, if any. - `default_deployment` (`string | null`): The deployment their conversations use when none is named (dep_...); null: your company's default. - `memory_mode` (`"vestra" | "customer" | "none"`): Where Bash's memory of this user lives: `vestra`, `customer` (your memory endpoint), or `none`. - `is_service_user` (`boolean`): true for a user Vestra keeps for itself, such as your company's own Bash; false for your users. - `created_at` (`string`): When the user was made, as an ISO 8601 time. - `spend_today_usd` (`number`): What they have spent today (UTC), in billed US dollars. Example: `1.24`. - `spend_month_usd` (`number`): What they have spent this calendar month (UTC), in billed US dollars. Example: `18.6`. - `caps_remaining` (`object`): What is left under every cap that applies to them: the smaller of their own and your company's. - `day_usd` (`number | null`): What they may still spend today, in US dollars; null when no cap applies. - `month_usd` (`number | null`): What they may still spend this month, in US dollars; null when no cap applies. ## Code **TypeScript** ```ts const traveller = await vestra.user('traveller-42').get(); ``` **Python** ```python traveller = vestra.user("traveller-42").get() ``` **CLI** ```bash vestra users traveller-42 ``` **HTTP** ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` --- url: https://vestra.ai/developers/reference/api/users/patch-v1-users-user-id.md --- # PATCH /v1/users/{user_id} > Change name, language, time zone, metadata, day or month spend cap, default deployment, or status (active or disabled). In [Users API](https://vestra.ai/developers/reference/api/users.md): Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. Change name, language, time zone, metadata, day or month spend cap, default deployment, or status (active or disabled). Your API key, scope `admin` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Every call here is a checked sample. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. Body: - `name` (`string | null`): A new name; null clears it. Limits: 1 to 200 characters. - `email` (`string (email) | null`): A new email address; null clears it. Limits: at most 320 characters. - `locale` (`string | null`): A new language and region (BCP 47); null clears it. Limits: 2 to 35 characters. - `timezone` (`string | null`): A new time zone (IANA); null clears it, and their routines then run in UTC. Limits: 1 to 64 characters. - `metadata` (`map of any`): Your facts about the user: this takes the place of what was there. - `spend_cap_usd` (`number | null`): Their own cap per day, in US dollars, never above your company's; null goes back to your company's cap for each user. Limits: 0 to 1000000. - `monthly_cap_usd` (`number | null`): Their own cap per calendar month, in US dollars; null goes back to your company's cap for each user. Limits: 0 to 1000000. - `default_deployment` (`string | null`): The deployment their conversations use when none is named: its id or name; null goes back to your company's default. Limits: at least 1 character. - `status` (`"active" | "disabled"`): disabled refuses their calls (403 user_disabled) until you set active again. ## Answers 200 - `id` (`string`): Your own id for the user. Example: `traveller-42`. - `object` (`"user"`): Always user. - `name` (`string | null`): The user's name; null when you gave none. - `email` (`string | null`): The user's email address; null when you gave none. - `locale` (`string | null`): The user's language and region (BCP 47); null when you gave none. - `timezone` (`string | null`): The user's time zone (IANA); null when you gave none, and their routines then run in UTC. - `metadata` (`map of any`): Your own facts about the user; empty when you gave none. - `status` (`"active" | "disabled"`): active, or disabled: a disabled user's calls are refused (403 user_disabled) until you turn them back on. - `spend_cap_usd` (`number | null`): The user's own cap per day, in billed US dollars, never above your company's; null: your company's cap for each user, if any. - `monthly_cap_usd` (`number | null`): The user's own cap per calendar month (UTC), in billed US dollars; null: your company's cap for each user, if any. - `default_deployment` (`string | null`): The deployment their conversations use when none is named (dep_...); null: your company's default. - `memory_mode` (`"vestra" | "customer" | "none"`): Where Bash's memory of this user lives: `vestra`, `customer` (your memory endpoint), or `none`. - `is_service_user` (`boolean`): true for a user Vestra keeps for itself, such as your company's own Bash; false for your users. - `created_at` (`string`): When the user was made, as an ISO 8601 time. - `spend_today_usd` (`number`): What they have spent today (UTC), in billed US dollars. Example: `1.24`. - `spend_month_usd` (`number`): What they have spent this calendar month (UTC), in billed US dollars. Example: `18.6`. - `caps_remaining` (`object`): What is left under every cap that applies to them: the smaller of their own and your company's. - `day_usd` (`number | null`): What they may still spend today, in US dollars; null when no cap applies. - `month_usd` (`number | null`): What they may still spend this month, in US dollars; null when no cap applies. ## Code **TypeScript** ```ts await vestra.user('traveller-42').update({ monthly_cap_usd: 20 }); ``` **Python** ```python vestra.user("traveller-42").update(monthly_cap_usd=20) ``` **CLI** ```bash vestra users traveller-42 --cap 20 ``` **HTTP** ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` --- url: https://vestra.ai/developers/reference/api/users/delete-v1-users-user-id.md --- # DELETE /v1/users/{user_id} > Erase the user and everything Bash holds for them: computer, files, memory. In [Users API](https://vestra.ai/developers/reference/api/users.md): Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. Erase the user and everything Bash holds for them: computer, files, memory. Runs as a job; a webhook says when it is done. Your API key, scope `admin` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Every call here is a checked sample. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. ## Answers 202 - `id` (`string`): Your own id for the user. - `object` (`"user"`): Always user. - `deleted` (`false`): Always false here: erasing runs in the background, and user.deleted says when it is done. - `status` (`"deleting"`): Always deleting. ## Code **TypeScript** ```ts await vestra.user('traveller-42').delete(); ``` **Python** ```python vestra.user("traveller-42").delete() ``` **CLI** ```bash vestra users delete traveller-42 --yes ``` **HTTP** ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` --- url: https://vestra.ai/developers/reference/api/users/post-v1-users-user-id-export.md --- # POST /v1/users/{user_id}/export > Package everything held for the user into one download, for data-rights requests. In [Users API](https://vestra.ai/developers/reference/api/users.md): Your users: set one up, read their spend, change their cap, export or erase them. Send `Authorization: Bearer vk_test_...` on every call. Package everything held for the user into one download, for data-rights requests. Your API key, scope `admin` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. ## Answers 200 - `object` (`"user_export"`): Always user_export. - `url` (`string`): A link to everything Vestra holds for the user, as one JSON file: good for 15 minutes. - `expires_at` (`string`): When the link stops working, as an ISO 8601 time. - `bytes` (`integer`): The size of the file, in bytes. - `truncated` (`boolean`): true when the user has more than the 200 tasks it holds: the oldest are left out. ## Code **TypeScript** ```ts const download = await vestra.user('traveller-42').export(); ``` **Python** ```python download = vestra.user("traveller-42").export() ``` **CLI** ```bash vestra users export traveller-42 ``` **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/users/{user_id}/export" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/conversations.md --- # Conversations API > Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/conversations](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations.md): Ask. - `GET` [/v1/conversations](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations.md): Conversations, newest first: one user's with user_id, or every one of the company's without it (the company's own Bash shows user_id null). - `GET` [/v1/conversations/{conversation_id}](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id.md): One conversation: status (idle, replying, working, waiting), its open work, its open approvals. - `POST` [/v1/conversations/{conversation_id}/messages](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-messages.md): Send a message. - `GET` [/v1/conversations/{conversation_id}/messages](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-messages.md): The conversation, paged: your messages, Bash's replies, and summaries of the work it did. - `GET` [/v1/conversations/{conversation_id}/events](https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-events.md): The one stream (SSE): replies as they are typed, work started and finished, progress, approvals, tool calls, files. - `POST` [/v1/conversations/{conversation_id}/stop](https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-stop.md): Stop the reply and every piece of work the conversation started. --- url: https://vestra.ai/developers/reference/api/conversations/post-v1-conversations.md --- # POST /v1/conversations > Ask. In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. Ask. Start a conversation, optionally with its first message and that message's options (context, attachments, output_schema, success_criteria, tools, hand_off). user_id names whose Bash it is, created on first use; leave it out for the company's own Bash. stream: true answers the first turn's events as text/event-stream and ends when the turn settles. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, if you send one: - `user_id` (`string`): Your id for the user this conversation is for; Vestra makes the user the first time you name them. Absent: your company's own Bash. Example: `traveller-42`. - `deployment` (`string`): Which deployment answers: its id (dep_...) or its name. Absent: the user's default deployment, then your company's. Limits: at least 1 character. Example: `travel-help`. - `metadata` (`map of string`): Your own labels, given back as you sent them: at most 32 keys of 1 to 64 characters, each value a string of at most 512. Bash never reads them. Example: `{"order_id":"ORD-1042","channel":"app"}`. - `message` (`string`): A first message, in at most 32,000 characters. Absent, with no attachment: the conversation starts empty. Limits: at most 32000 characters. Example: `When does my flight on TN-48213 leave?`. - `context` (`map of any`): Facts for this message only, as JSON. Bash reads them as data, never as instructions. Example: `{"booking_reference":"TN-48213","tier":"gold"}`. - `attachments` (`object[]`): Files Bash should read with this message: at most 10. Limits: at most 10 items. - `file_id` (`string`, required): A file you uploaded with POST /v1/files (file_...). Limits: at least 1 character. - `output_schema` (`map of any`): A JSON Schema, at most 32 KB, for the data you want back: the turn's data matches it. Example: `{"type":"object","properties":{"flight":{"type":"string"}},"required":["flight"]}`. - `success_criteria` (`string`): What a good result looks like, in your words: when the turn settles, its outcome says whether it was met, and why. Limits: 1 to 4000 characters. Example: `A new booking on a flight that leaves before noon.`. - `tools` (`object[]`): Tools your code answers, for this message only, on top of the deployment's: at most 32. Limits: at most 32 items. - `name` (`string`, required): The tool's name: lowercase letters, digits and underscores, starting with a letter, up to 64. Example: `get_booking`. - `description` (`string`, required): What the tool does, so Bash knows when to call it, up to 1024 characters. Limits: 1 to 1024 characters. Example: `A booking by its PNR`. - `input_schema` (`map of any`, required): Its arguments, as JSON Schema with `type: object`. Bash's call fills them in. Example: `{"type":"object","properties":{"pnr":{"type":"string"}},"required":["pnr"]}`. - `timeout_s` (`integer`): Seconds Bash waits for your answer, 5 to 600. Default 120; after it, the call expires. Default: `120`. Limits: 5 to 600. - `read_only` (`boolean`): true: the tool changes nothing (a lookup), so Bash calls it without waiting for the person's yes. Absent: false. - `run_without_asking` (`boolean`): true: the tool changes something that needs no yes, so Bash calls it without waiting for the person's yes. Absent: false. - `hand_off` (`"auto" | "always"`): For the first message: auto lets Bash reply first; always sends it straight to work. Default: auto. - `stream` (`boolean`): true answers with the turn's events as they happen (text/event-stream), ending when the turn settles. Default: one JSON answer. ## Answers 201 - `id` (`string`): The conversation (conv_...). Example: `conv_8f2k`. - `object` (`"conversation"`): Always conversation. - `user_id` (`string | null`): Your id for the user; null on your company's own Bash, and on an agent's conversation over A2A that names no user. - `deployment_id` (`string | null`): The deployment that answers it (dep_...); null when it has none. - `deployment_version` (`integer | null`): The version of the deployment it runs on: in test mode the one it started on, in live mode the one live at its last message. Null with no deployment. - `status` (`"idle" | "replying" | "working" | "waiting"`): What the conversation is doing. idle: nothing. replying: Bash is writing its reply. working: work Bash started is running. waiting: Bash waits on an approval, a question or a call to one of your tools. - `metadata` (`map of string`): Your labels, as you sent them; empty when you sent none. - `created_at` (`string`): When it started, as an ISO 8601 time. - `last_active_at` (`string`): When a message or its work last changed it, as an ISO 8601 time. - `open_work` (`object[]`): Work Bash started that has not ended, oldest first. - `task_id` (`string`): The task (task_...). - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): Where a piece of work is. queued: not started yet. working: running on the user's computer. waiting: it waits on an approval or a question. done or failed: it has ended. - `open_interactions` (`object[]`): Approvals and questions Bash waits on. - `interaction_id` (`string`): The approval or question, to answer by its id (int_...). - `task_id` (`string`): The task that asked (task_...). - `kind` (`string`): What it asks for, as stored: such as tool_permission, ask_question or composio_auth. - `pending_tool_calls` (`object[]`): Calls to your tools that wait on your answer. - `call_id` (`string`): The call, to answer by its id (call_...). - `name` (`string`): The tool Bash called. - `arguments` (`any`): What Bash passed, matching the input schema of the tool. - `task_id` (`string | null`): The task that made the call; null when Bash's reply made it. - `last_event_seq` (`integer`): The seq of the newest event on its stream: read the conversation, then follow its events after this seq, and you miss nothing. - `cost_usd` (`number`): What it has cost so far, in billed US dollars: Bash's replies, all its work and its voice calls. Example: `0.0412`. - `cost_breakdown` (`object`): The same cost, by kind of spend. - `orchestrator` (`number`): Bash's own replies in the conversation, in billed US dollars; 0 on a piece of work. - `intelligence` (`number`): The thinking Bash did while it worked, in billed US dollars. - `tools` (`number`): The tools and apps Bash used, in billed US dollars. - `compute` (`number`): Time on Bash's computer, in billed US dollars. - `background` (`number`): Work Bash did in the background, such as remembering and tidying, in billed US dollars. - `voice` (`number`): Voice call time, in billed US dollars. - `turn_id` (`string`, may be absent): The turn of the first message, when you sent one (turn_...). ## Code **TypeScript** ```ts const reply = await vestra.user('traveller-42').ask('My 6am flight got cancelled. Get me on the next one.').text(); ``` **Python** ```python reply = vestra.user("traveller-42").ask("My 6am flight got cancelled. Get me on the next one.").text() ``` **CLI** ```bash vestra ask "My 6am flight got cancelled. Get me on the next one." --user traveller-42 ``` **HTTP** ```bash # Ask for one of your users. Their Bash is created the first time you name them. With stream: true, the # response streams this turn's events as they happen, and ends when the turn is over. 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. What should I do?", "stream": true }' # Ask a follow-up. It goes to the same conversation. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Can I get a refund for it?", "stream": true }' # Stop a reply and its work. This stops the reply and every task the conversation started. curl -X POST "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/stop" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Ask for your company. Leave user_id out, and the job goes to your company's own Bash. curl "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Write a two-line notice for our travellers about storm delays at Mumbai airport today." }' # Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, # that item waits for the answer. curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "A storm may delay flights from Mumbai today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." }, { "user_id": "traveller-77", "message": "A storm may delay flights from Pune today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." } ] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` --- url: https://vestra.ai/developers/reference/api/conversations/get-v1-conversations.md --- # GET /v1/conversations > Conversations, newest first: one user's with user_id, or every one of the company's without it (the company's own Bash shows user_id null). In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. Conversations, newest first: one user's with user_id, or every one of the company's without it (the company's own Bash shows user_id null). Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): Only this user's conversations, by your id for them. Absent: every conversation of your company. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The conversation (conv_...). Example: `conv_8f2k`. - `object` (`"conversation"`): Always conversation. - `user_id` (`string | null`): Your id for the user; null on your company's own Bash, and on an agent's conversation over A2A that names no user. - `deployment_id` (`string | null`): The deployment that answers it (dep_...); null when it has none. - `deployment_version` (`integer | null`): The version of the deployment it runs on: in test mode the one it started on, in live mode the one live at its last message. Null with no deployment. - `status` (`"idle" | "replying" | "working" | "waiting"`): What the conversation is doing. idle: nothing. replying: Bash is writing its reply. working: work Bash started is running. waiting: Bash waits on an approval, a question or a call to one of your tools. - `metadata` (`map of string`): Your labels, as you sent them; empty when you sent none. - `created_at` (`string`): When it started, as an ISO 8601 time. - `last_active_at` (`string`): When a message or its work last changed it, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id.md --- # GET /v1/conversations/{conversation_id} > One conversation: status (idle, replying, working, waiting), its open work, its open approvals. In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. One conversation: status (idle, replying, working, waiting), its open work, its open approvals. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Path: - `conversation_id` (`string`, required): The conversation's id (`conv_...`). ## Answers 200 - `id` (`string`): The conversation (conv_...). Example: `conv_8f2k`. - `object` (`"conversation"`): Always conversation. - `user_id` (`string | null`): Your id for the user; null on your company's own Bash, and on an agent's conversation over A2A that names no user. - `deployment_id` (`string | null`): The deployment that answers it (dep_...); null when it has none. - `deployment_version` (`integer | null`): The version of the deployment it runs on: in test mode the one it started on, in live mode the one live at its last message. Null with no deployment. - `status` (`"idle" | "replying" | "working" | "waiting"`): What the conversation is doing. idle: nothing. replying: Bash is writing its reply. working: work Bash started is running. waiting: Bash waits on an approval, a question or a call to one of your tools. - `metadata` (`map of string`): Your labels, as you sent them; empty when you sent none. - `created_at` (`string`): When it started, as an ISO 8601 time. - `last_active_at` (`string`): When a message or its work last changed it, as an ISO 8601 time. - `open_work` (`object[]`): Work Bash started that has not ended, oldest first. - `task_id` (`string`): The task (task_...). - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): Where a piece of work is. queued: not started yet. working: running on the user's computer. waiting: it waits on an approval or a question. done or failed: it has ended. - `open_interactions` (`object[]`): Approvals and questions Bash waits on. - `interaction_id` (`string`): The approval or question, to answer by its id (int_...). - `task_id` (`string`): The task that asked (task_...). - `kind` (`string`): What it asks for, as stored: such as tool_permission, ask_question or composio_auth. - `pending_tool_calls` (`object[]`): Calls to your tools that wait on your answer. - `call_id` (`string`): The call, to answer by its id (call_...). - `name` (`string`): The tool Bash called. - `arguments` (`any`): What Bash passed, matching the input schema of the tool. - `task_id` (`string | null`): The task that made the call; null when Bash's reply made it. - `last_event_seq` (`integer`): The seq of the newest event on its stream: read the conversation, then follow its events after this seq, and you miss nothing. - `cost_usd` (`number`): What it has cost so far, in billed US dollars: Bash's replies, all its work and its voice calls. Example: `0.0412`. - `cost_breakdown` (`object`): The same cost, by kind of spend. - `orchestrator` (`number`): Bash's own replies in the conversation, in billed US dollars; 0 on a piece of work. - `intelligence` (`number`): The thinking Bash did while it worked, in billed US dollars. - `tools` (`number`): The tools and apps Bash used, in billed US dollars. - `compute` (`number`): Time on Bash's computer, in billed US dollars. - `background` (`number`): Work Bash did in the background, such as remembering and tidying, in billed US dollars. - `voice` (`number`): Voice call time, in billed US dollars. ## Code **HTTP** ```bash # The conversation answers what it has cost so far. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # The receipt: every charge behind it, newest first. curl "$VESTRA_BASE_URL/v1/usage/line-items?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-messages.md --- # POST /v1/conversations/{conversation_id}/messages > Send a message. In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. Send a message. Options: context (data for this turn only), attachments, output_schema, success_criteria, tools, hand_off (auto or always). Answers the turn at once; Prefer: wait=N holds for the final answer; stream: true answers this turn's events as text/event-stream and ends when the turn settles. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Path: - `conversation_id` (`string`, required): The conversation's id (`conv_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. - `Prefer` (`string`): wait=N (N at most 120) holds the call until the turn and its work finish. Body: - `message` (`string`): What you or your user say to Bash, in at most 32,000 characters. Default: empty, for a send that carries only attachments. Default: empty. Limits: at most 32000 characters. Example: `My 6am flight got cancelled. Book me on the next one.`. - `context` (`map of any`): Facts for this message only, as JSON. Bash reads them as data, never as instructions. Example: `{"booking_reference":"TN-48213","tier":"gold"}`. - `attachments` (`object[]`): Files Bash should read with this message: at most 10. Limits: at most 10 items. - `file_id` (`string`, required): A file you uploaded with POST /v1/files (file_...). Limits: at least 1 character. - `output_schema` (`map of any`): A JSON Schema, at most 32 KB, for the data you want back: the turn's data matches it. Example: `{"type":"object","properties":{"flight":{"type":"string"}},"required":["flight"]}`. - `success_criteria` (`string`): What a good result looks like, in your words: when the turn settles, its outcome says whether it was met, and why. Limits: 1 to 4000 characters. Example: `A new booking on a flight that leaves before noon.`. - `tools` (`object[]`): Tools your code answers, for this message only, on top of the deployment's: at most 32. Limits: at most 32 items. - `name` (`string`, required): The tool's name: lowercase letters, digits and underscores, starting with a letter, up to 64. Example: `get_booking`. - `description` (`string`, required): What the tool does, so Bash knows when to call it, up to 1024 characters. Limits: 1 to 1024 characters. Example: `A booking by its PNR`. - `input_schema` (`map of any`, required): Its arguments, as JSON Schema with `type: object`. Bash's call fills them in. Example: `{"type":"object","properties":{"pnr":{"type":"string"}},"required":["pnr"]}`. - `timeout_s` (`integer`): Seconds Bash waits for your answer, 5 to 600. Default 120; after it, the call expires. Default: `120`. Limits: 5 to 600. - `read_only` (`boolean`): true: the tool changes nothing (a lookup), so Bash calls it without waiting for the person's yes. Absent: false. - `run_without_asking` (`boolean`): true: the tool changes something that needs no yes, so Bash calls it without waiting for the person's yes. Absent: false. - `hand_off` (`"auto" | "always"`): How a message reaches Bash. auto: Bash reads it, replies, and starts work when the job needs it. always: it goes straight to work, as a task; the reply is the one line Started work on it. Default: `auto`. - `metadata` (`map of string`): Your own labels, given back as you sent them: at most 32 keys of 1 to 64 characters, each value a string of at most 512. Bash never reads them. Example: `{"order_id":"ORD-1042","channel":"app"}`. - `stream` (`boolean`): true answers with the turn's events as they happen (text/event-stream), ending when the turn settles. Default: one JSON answer. ## Answers 200 or 202 - `id` (`string`): The turn: one message and all it led to (turn_...). - `object` (`"conversation.turn"`): Always conversation.turn. - `conversation_id` (`string`): Its conversation (conv_...). - `status` (`"running" | "working" | "requires_action" | "completed" | "failed" | "stopped"`): Where one message is. running: Bash is replying. working: the work it started is open. requires_action: it waits on a call to one of your tools. completed, failed or stopped: it has settled. - `reply` (`string | null`): Bash's reply; null until it is written. For a message sent straight to work it is the one line Started work on it. - `answer` (`string | null`): The reply's answer: its final words, the part after any search or lookup Bash made on the way, trimmed; null until the reply is written, or when it said nothing. A turn from before this field holds its whole reply. When Bash went to work, the work's own answer is GET /v1/tasks/{id}'s result.text. - `data` (`any | null`): The data you asked for with output_schema, matching it; null when you asked for none. - `outcome` (`object | null`): Your success_criteria, judged once the turn and its work settle; null when you gave none. - `met` (`boolean`): Whether the result meets your success_criteria. - `reason` (`string`): Why, in a sentence. - `work` (`object[]`): The work this turn started. - `task_id` (`string`): The task (task_...). - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): Where a piece of work is. queued: not started yet. working: running on the user's computer. waiting: it waits on an approval or a question. done or failed: it has ended. - `result` (`string | null`): Its answer, its final reply, cut to 280 characters; null until it has any. The whole answer is GET /v1/tasks/{id}'s result.text. - `tool_calls` (`object[]`): While it requires action: the calls to your tools it waits on. Empty otherwise. - `call_id` (`string`): The call, to answer by its id (call_...). - `name` (`string`): The tool Bash called. - `arguments` (`any`): What Bash passed, matching the input schema of the tool. - `task_id` (`string | null`): The task that made the call; null when Bash's reply made it. - `error` (`object | null`): Why the turn failed; null unless it did. - `code` (`string`): Why it failed, as a code: output_invalid (the answer did not match output_schema), work_lost (your answer never reached the work), process_lost (our side was cut off mid-reply; send it again), or Bash's own: credits_exhausted, model_timeout, tool_failed, bad_tool_args, loop_exhausted, unauthorized or internal. - `message` (`string`): What went wrong, in plain words. - `created_at` (`string`): When the message was sent, as an ISO 8601 time. - `finished_at` (`string | null`): When the turn settled, as an ISO 8601 time; null while it runs. ## Code **TypeScript** ```ts const reply = await vestra.user('traveller-42').ask('Make it a window seat.', { conversation: 'conv_8f2k' }).text(); ``` **Python** ```python reply = vestra.user("traveller-42").ask("Make it a window seat.", conversation="conv_8f2k").text() ``` **CLI** ```bash vestra ask "Make it a window seat." --user traveller-42 --conversation conv_8f2k ``` **HTTP** ```bash # Ask for one of your users. Their Bash is created the first time you name them. With stream: true, the # response streams this turn's events as they happen, and ends when the turn is over. 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. What should I do?", "stream": true }' # Ask a follow-up. It goes to the same conversation. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Can I get a refund for it?", "stream": true }' # Stop a reply and its work. This stops the reply and every task the conversation started. curl -X POST "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/stop" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Ask for your company. Leave user_id out, and the job goes to your company's own Bash. curl "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Write a two-line notice for our travellers about storm delays at Mumbai airport today." }' # Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, # that item waits for the answer. curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "A storm may delay flights from Mumbai today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." }, { "user_id": "traveller-77", "message": "A storm may delay flights from Pune today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." } ] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` --- url: https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-messages.md --- # GET /v1/conversations/{conversation_id}/messages > The conversation, paged: your messages, Bash's replies, and summaries of the work it did. In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. The conversation, paged: your messages, Bash's replies, and summaries of the work it did. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `conversation_id` (`string`, required): The conversation's id (`conv_...`). Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The line. - `object` (`"conversation.message"`): Always conversation.message. - `turn_id` (`string`): The turn it belongs to (turn_...). - `role` (`"user" | "assistant"`): user: what was sent. assistant: what Bash said back. - `text` (`string`): The words. A send of tool results reads "Tool results:" and the tools it answered. - `work` (`object[]`): On Bash's line: the work its reply started. - `task_id` (`string`): The task (task_...). - `title` (`string | null`): What the work is; null until Bash names it. - `created_at` (`string`): When it was said, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/conversations/{conversation_id}/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/conversations/get-v1-conversations-conversation-id-events.md --- # GET /v1/conversations/{conversation_id}/events > The one stream (SSE): replies as they are typed, work started and finished, progress, approvals, tool calls, files. In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. The one stream (SSE): replies as they are typed, work started and finished, progress, approvals, tool calls, files. Resume with Last-Event-ID. Filter to one task's events with task_id. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Every call here is a checked sample. ## Request Path: - `conversation_id` (`string`, required): The conversation's id (`conv_...`). Query: - `format` (`"sse" | "json"`): sse follows the events as they happen (text/event-stream); json reads one page of them. Default: sse. - `after` (`integer`): Only events after this seq. On the stream, a Last-Event-ID header does the same. Absent: from the first kept event. - `limit` (`integer`): With format json: at most this many events, 1 to 500. Default: 100. - `task_id` (`string`): Only this task's events (task_...). Headers: - `Last-Event-ID` (`string`): Resume after this event id. ## Answers 200 A stream of events; with format=json, a page of them. - `data` (`object[]`): The events, oldest first. - `seq` (`integer`): Its place on the conversation's stream, counting up: send the last one you saw to pick up after it. - `type` (`string`): What happened, from the one catalog of events: the events reference says what each carries and where it is delivered. Limits: one of 34 values. Example: `message.completed`. - `data` (`map of any`): What it says: its fields depend on its type, as the events reference lists them. - `created_at` (`string`): When it happened, as an ISO 8601 time. - `next_after` (`integer`): The seq to send as after for the next page: the last one here, or the after you sent (0 without one) when the page is empty. ## Code **TypeScript** ```ts for await (const event of vestra.events('conv_8f2k')) { if (event.type === 'message.completed') break; } ``` **Python** ```python for event in vestra.events("conv_8f2k"): if event.type == "message.completed": break ``` **CLI** ```bash vestra events conv_8f2k ``` **HTTP** ```bash # Stream a conversation live. If the connection drops, 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: 41" # Read the same events as JSON. You get them in pages, after the seq you give. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events?format=json&after=41" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Read a task afterwards. You get every step of one task, with tokens and cost per turn. curl "$VESTRA_BASE_URL/v1/tasks/task_91d/trace" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Send events to your server. Register a URL and the events it wants. The signing secret is shown only once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created", "usage.cap_reached"] }' # Catch up on what you missed. List the events your webhooks were sent, newest first. curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } } } ``` --- url: https://vestra.ai/developers/reference/api/conversations/post-v1-conversations-conversation-id-stop.md --- # POST /v1/conversations/{conversation_id}/stop > Stop the reply and every piece of work the conversation started. In [Conversations API](https://vestra.ai/developers/reference/api/conversations.md): Ask Bash in a conversation, follow its reply as it happens, and stop it. Send `Authorization: Bearer vk_test_...` on every call. Stop the reply and every piece of work the conversation started. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Path: - `conversation_id` (`string`, required): The conversation's id (`conv_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 200 - `id` (`string`): The conversation (conv_...). - `object` (`"conversation.stop"`): Always conversation.stop. - `stopped_work` (`string[]`): The tasks the stop ended (task_...). ## Code **TypeScript** ```ts await vestra.user('traveller-42').ask('Find me a hotel near the airport.').stop(); ``` **Python** ```python vestra.user("traveller-42").ask("Find me a hotel near the airport.").stop() ``` **CLI** ```bash vestra ask --stop conv_8f2k ``` **HTTP** ```bash # Ask for one of your users. Their Bash is created the first time you name them. With stream: true, the # response streams this turn's events as they happen, and ends when the turn is over. 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. What should I do?", "stream": true }' # Ask a follow-up. It goes to the same conversation. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Can I get a refund for it?", "stream": true }' # Stop a reply and its work. This stops the reply and every task the conversation started. curl -X POST "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/stop" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Ask for your company. Leave user_id out, and the job goes to your company's own Bash. curl "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Write a two-line notice for our travellers about storm delays at Mumbai airport today." }' # Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, # that item waits for the answer. curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "A storm may delay flights from Mumbai today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." }, { "user_id": "traveller-77", "message": "A storm may delay flights from Pune today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." } ] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` --- url: https://vestra.ai/developers/reference/api/work.md --- # Work API > The work Bash does on its computer: list it, read it, stop it or delete it. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/tasks](https://vestra.ai/developers/reference/api/work/get-v1-tasks.md): All work across your users, or one user's with user_id. - `GET` [/v1/tasks/{task_id}](https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id.md): Status, title, the progress list, the result (text, and data when an output_schema was set), the outcome, cost so far. - `DELETE` [/v1/tasks/{task_id}](https://vestra.ai/developers/reference/api/work/delete-v1-tasks-task-id.md): Delete it and its outputs. - `GET` [/v1/tasks/{task_id}/trace](https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id-trace.md): Every step: tools called with a summary of inputs and outputs, timings, tokens and cost per turn. - `POST` [/v1/tasks/{task_id}/stop](https://vestra.ai/developers/reference/api/work/post-v1-tasks-task-id-stop.md): Stop this piece of work only. --- url: https://vestra.ai/developers/reference/api/work/get-v1-tasks.md --- # GET /v1/tasks > All work across your users, or one user's with user_id. In [Work API](https://vestra.ai/developers/reference/api/work.md): The work Bash does on its computer: list it, read it, stop it or delete it. Send `Authorization: Bearer vk_test_...` on every call. All work across your users, or one user's with user_id. Filter by status, deployment, conversation or date too. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): Only tasks with this status. - `user_id` (`string`): Only this user's, by your id for them. - `deployment_id` (`string`): Only this deployment's (dep_...). - `conversation_id` (`string`): Only this conversation's (conv_...). - `created_after` (`string (date and time)`): Only tasks started after this ISO 8601 time. - `created_before` (`string (date and time)`): Only tasks started before this ISO 8601 time. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The task: one piece of work Bash started (task_...). Example: `task_91d`. - `object` (`"task"`): Always task. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): The conversation that started it (conv_...); null when none did. - `deployment_id` (`string | null`): The deployment it runs on (dep_...); null when none. - `deployment_version` (`integer | null`): The version of that deployment it runs on; null when none. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): queued: not started yet. working: running on the user's computer. waiting: it waits on an approval or a question. done or failed: it has ended. - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `progress` (`object[]`): Its plan, as its latest to-do list; empty when it has none. - `content` (`string`): One step of its plan. - `status` (`string`): Where that step is: pending, in_progress or completed. - `result` (`object`): What the work came to. - `text` (`string | null`): The work's answer: its final reply. Read one task for all of it; a list cuts it to 280 characters, as does a task that ended before its answer was recorded. Null until it has any, or when the work ended with no final reply. - `data` (`any | null`): Data it returned for an output_schema; null when it returned none. - `outcome` (`object | null`): Whether it met the success criteria of its message; null when there were none. - `met` (`boolean`): Whether it met the success criteria. - `reason` (`string`): Why, in a sentence. - `cost_usd` (`number`): What it has cost so far, in billed US dollars; the last charges can land a few seconds after it ends. Example: `0.0412`. - `cost_breakdown` (`object`): The same cost, by kind of spend. - `orchestrator` (`number`): Bash's own replies in the conversation, in billed US dollars; 0 on a piece of work. - `intelligence` (`number`): The thinking Bash did while it worked, in billed US dollars. - `tools` (`number`): The tools and apps Bash used, in billed US dollars. - `compute` (`number`): Time on Bash's computer, in billed US dollars. - `background` (`number`): Work Bash did in the background, such as remembering and tidying, in billed US dollars. - `voice` (`number`): Voice call time, in billed US dollars. - `created_at` (`string`): When it started, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/tasks" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id.md --- # GET /v1/tasks/{task_id} > Status, title, the progress list, the result (text, and data when an output_schema was set), the outcome, cost so far. In [Work API](https://vestra.ai/developers/reference/api/work.md): The work Bash does on its computer: list it, read it, stop it or delete it. Send `Authorization: Bearer vk_test_...` on every call. Status, title, the progress list, the result (text, and data when an output_schema was set), the outcome, cost so far. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `task_id` (`string`, required): The piece of work's id (`task_...`). ## Answers 200 - `id` (`string`): The task: one piece of work Bash started (task_...). Example: `task_91d`. - `object` (`"task"`): Always task. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): The conversation that started it (conv_...); null when none did. - `deployment_id` (`string | null`): The deployment it runs on (dep_...); null when none. - `deployment_version` (`integer | null`): The version of that deployment it runs on; null when none. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): queued: not started yet. working: running on the user's computer. waiting: it waits on an approval or a question. done or failed: it has ended. - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `progress` (`object[]`): Its plan, as its latest to-do list; empty when it has none. - `content` (`string`): One step of its plan. - `status` (`string`): Where that step is: pending, in_progress or completed. - `result` (`object`): What the work came to. - `text` (`string | null`): The work's answer: its final reply. Read one task for all of it; a list cuts it to 280 characters, as does a task that ended before its answer was recorded. Null until it has any, or when the work ended with no final reply. - `data` (`any | null`): Data it returned for an output_schema; null when it returned none. - `outcome` (`object | null`): Whether it met the success criteria of its message; null when there were none. - `met` (`boolean`): Whether it met the success criteria. - `reason` (`string`): Why, in a sentence. - `cost_usd` (`number`): What it has cost so far, in billed US dollars; the last charges can land a few seconds after it ends. Example: `0.0412`. - `cost_breakdown` (`object`): The same cost, by kind of spend. - `orchestrator` (`number`): Bash's own replies in the conversation, in billed US dollars; 0 on a piece of work. - `intelligence` (`number`): The thinking Bash did while it worked, in billed US dollars. - `tools` (`number`): The tools and apps Bash used, in billed US dollars. - `compute` (`number`): Time on Bash's computer, in billed US dollars. - `background` (`number`): Work Bash did in the background, such as remembering and tidying, in billed US dollars. - `voice` (`number`): Voice call time, in billed US dollars. - `created_at` (`string`): When it started, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/tasks/{task_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/work/delete-v1-tasks-task-id.md --- # DELETE /v1/tasks/{task_id} > Delete it and its outputs. In [Work API](https://vestra.ai/developers/reference/api/work.md): The work Bash does on its computer: list it, read it, stop it or delete it. Send `Authorization: Bearer vk_test_...` on every call. Delete it and its outputs. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `task_id` (`string`, required): The piece of work's id (`task_...`). ## Answers 200 - `id` (`string`): The task (task_...). - `object` (`"task"`): Always task. - `deleted` (`true`): Always true: it is gone, stopped first if it was running. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/tasks/{task_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/work/get-v1-tasks-task-id-trace.md --- # GET /v1/tasks/{task_id}/trace > Every step: tools called with a summary of inputs and outputs, timings, tokens and cost per turn. In [Work API](https://vestra.ai/developers/reference/api/work.md): The work Bash does on its computer: list it, read it, stop it or delete it. Send `Authorization: Bearer vk_test_...` on every call. Every step: tools called with a summary of inputs and outputs, timings, tokens and cost per turn. Your API key, scope `runtime` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Every call here is a checked sample. ## Request Path: - `task_id` (`string`, required): The piece of work's id (`task_...`). ## Answers 200 - `object` (`"task_trace"`): Always task_trace. - `task_id` (`string`): The task (task_...). - `steps` (`object[]`): Every tool Bash used, in order. - `tool_use_id` (`string`): The step: one use of a tool. - `name` (`string | null`): The tool Bash used; null when it was not recorded. - `input_summary` (`string | null`): What Bash did with it, in a line: never the raw input. Null when there is none. - `output_summary` (`string | null`): What came back, in a line; null when there is none. - `is_error` (`boolean`): Whether the step failed. - `started_at` (`string`): When it started, as an ISO 8601 time. - `finished_at` (`string | null`): When it finished, as an ISO 8601 time; null while running. - `duration_ms` (`number | null`): How long it took, in milliseconds; null while running. - `turns` (`object[]`): Every call to the model, in order, with its tokens and cost. - `turn_id` (`string`): The model turn: one call to the model. - `model` (`string | null`): The model that answered; null when it was not recorded. - `input_tokens` (`number`): Tokens the model read. - `output_tokens` (`number`): Tokens the model wrote. - `cache_read_tokens` (`number`): Of the tokens it read, how many came from the cache, at a lower price. - `cost_usd` (`number`): What this turn cost, in billed US dollars. - `duration_ms` (`number | null`): How long it took, in milliseconds; null when not recorded. - `status` (`string | null`): How it ended, as recorded; null when not recorded. - `started_at` (`string`): When it started, as an ISO 8601 time. - `totals` (`object`): The turns added up. - `input_tokens` (`number`): All tokens read. - `output_tokens` (`number`): All tokens written. - `cost_usd` (`number`): What every turn cost together, in billed US dollars. ## Code **TypeScript** ```ts const trace = await vestra.events.trace('task_91d'); ``` **Python** ```python trace = vestra.events.trace("task_91d") ``` **CLI** ```bash vestra events task_91d ``` **HTTP** ```bash # Stream a conversation live. If the connection drops, 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: 41" # Read the same events as JSON. You get them in pages, after the seq you give. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events?format=json&after=41" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Read a task afterwards. You get every step of one task, with tokens and cost per turn. curl "$VESTRA_BASE_URL/v1/tasks/task_91d/trace" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Send events to your server. Register a URL and the events it wants. The signing secret is shown only once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created", "usage.cap_reached"] }' # Catch up on what you missed. List the events your webhooks were sent, newest first. curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } } } ``` --- url: https://vestra.ai/developers/reference/api/work/post-v1-tasks-task-id-stop.md --- # POST /v1/tasks/{task_id}/stop > Stop this piece of work only. In [Work API](https://vestra.ai/developers/reference/api/work.md): The work Bash does on its computer: list it, read it, stop it or delete it. Send `Authorization: Bearer vk_test_...` on every call. Stop this piece of work only. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `task_id` (`string`, required): The piece of work's id (`task_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 200 - `task` (`object`): The task, after the stop. - `id` (`string`): The task: one piece of work Bash started (task_...). Example: `task_91d`. - `object` (`"task"`): Always task. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): The conversation that started it (conv_...); null when none did. - `deployment_id` (`string | null`): The deployment it runs on (dep_...); null when none. - `deployment_version` (`integer | null`): The version of that deployment it runs on; null when none. - `status` (`"queued" | "working" | "waiting" | "done" | "failed"`): queued: not started yet. working: running on the user's computer. waiting: it waits on an approval or a question. done or failed: it has ended. - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `progress` (`object[]`): Its plan, as its latest to-do list; empty when it has none. - `content` (`string`): One step of its plan. - `status` (`string`): Where that step is: pending, in_progress or completed. - `result` (`object`): What the work came to. - `text` (`string | null`): The work's answer: its final reply. Read one task for all of it; a list cuts it to 280 characters, as does a task that ended before its answer was recorded. Null until it has any, or when the work ended with no final reply. - `data` (`any | null`): Data it returned for an output_schema; null when it returned none. - `outcome` (`object | null`): Whether it met the success criteria of its message; null when there were none. - `met` (`boolean`): Whether it met the success criteria. - `reason` (`string`): Why, in a sentence. - `cost_usd` (`number`): What it has cost so far, in billed US dollars; the last charges can land a few seconds after it ends. Example: `0.0412`. - `cost_breakdown` (`object`): The same cost, by kind of spend. - `orchestrator` (`number`): Bash's own replies in the conversation, in billed US dollars; 0 on a piece of work. - `intelligence` (`number`): The thinking Bash did while it worked, in billed US dollars. - `tools` (`number`): The tools and apps Bash used, in billed US dollars. - `compute` (`number`): Time on Bash's computer, in billed US dollars. - `background` (`number`): Work Bash did in the background, such as remembering and tidying, in billed US dollars. - `voice` (`number`): Voice call time, in billed US dollars. - `created_at` (`string`): When it started, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. - `delivered` (`boolean`): Whether the stop surely reached the work. - `reason` (`"local_relay_down" | "nothing_to_kill" | "kill_not_permitted" | "not_running" | null`): Why the stop did not land; null when it did. not_running: it was at rest, so nothing was sent. nothing_to_kill: nothing was running. local_relay_down: it runs on the user's own computer, which cannot be reached. kill_not_permitted: it could not be reached, so it may still be running. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/tasks/{task_id}/stop" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/interactions.md --- # Interactions API > What Bash waits on (approvals, questions, apps to connect, hand-offs, calls to your tools) and the answers. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/interactions](https://vestra.ai/developers/reference/api/interactions/get-v1-interactions.md): Everything Bash waits on, across your users: approvals, questions, app connections, hand-offs, and calls to your client tools (type tool_call). - `GET` [/v1/interactions/{interaction_id}](https://vestra.ai/developers/reference/api/interactions/get-v1-interactions-interaction-id.md): One item, by its int_ or call_ id: its type (approval, question, connect, handoff, tool_call), what Bash wants to do, the options, when it expires. - `POST` [/v1/interactions/{interaction_id}/answer](https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-answer.md): Answer it, whatever it is. - `POST` [/v1/interactions/{interaction_id}/cancel](https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-cancel.md): Withdraw it. --- url: https://vestra.ai/developers/reference/api/interactions/get-v1-interactions.md --- # GET /v1/interactions > Everything Bash waits on, across your users: approvals, questions, app connections, hand-offs, and calls to your client tools (type tool_call). In [Interactions API](https://vestra.ai/developers/reference/api/interactions.md): What Bash waits on (approvals, questions, apps to connect, hand-offs, calls to your tools) and the answers. Send `Authorization: Bearer vk_test_...` on every call. Everything Bash waits on, across your users: approvals, questions, app connections, hand-offs, and calls to your client tools (type tool_call). Filter by user_id, conversation_id, task_id, type or status (pending by default). Your API key, scope `runtime` or `all`. On the card: [`answer`](https://vestra.ai/developers/answer.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): Only this user's, by your id for them. - `conversation_id` (`string`): Only this conversation's (conv_...). - `task_id` (`string`): Only this task's (task_...). - `type` (`"approval" | "question" | "connect" | "handoff" | "tool_call"`): Only this type. Absent: every type, calls to your tools included. tool_call: those calls alone. - `kind` (`"ask_question" | "composio_auth" | "tool_permission" | "credit_approval" | "exit_plan" | "custom" | "hands_approval" | "plan_limit"`): Only this kind. - `status` (`"pending" | "resolved" | "cancelled" | "expired"`): Only this status. Default: pending, what Bash waits on now. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"interaction"`): Always interaction. - `id` (`string`): The interaction, to answer by its id (int_...; call_... for a call to your tool). Example: `int_3j9`. - `user_id` (`string | null`): Your id for the user it is for; null on your company's own Bash. - `task_id` (`string | null`): The task that asked (task_...); null for a call to your tool that Bash's reply made. - `conversation_id` (`string | null`): Its conversation (conv_...); null when its task has none. - `kind` (`"ask_question" | "composio_auth" | "tool_permission" | "credit_approval" | "exit_plan" | "custom" | "hands_approval" | "plan_limit"`): What exactly Bash waits on. ask_question: a question, or a hand-off. composio_auth: connecting an app. tool_permission: a step with a tool. credit_approval: spending on a paid tool. exit_plan: a plan before it starts. hands_approval: an action on a computer, approved in person. plan_limit: a plan's limit ran out (read only). custom: anything else, named by details.renderer. - `type` (`"approval" | "question" | "connect" | "handoff" | "tool_call"`): What kind of answer it takes. approval: yes or no. question: an answer to each question. connect: your user connects an app. handoff: a person does one thing, then says so. tool_call: your tool returns an output or an error. - `status` (`"pending" | "resolved" | "cancelled" | "expired"`): pending: Bash waits on it. resolved: it was answered. cancelled: it was withdrawn before an answer. expired: nobody answered in time. - `summary` (`string`): What Bash wants, in a phrase to show your user. Example: `pay, amount $500.00`. - `moves_money` (`boolean | null`): Whether saying yes spends money, such as a payment. null on the card for one of your own tools (a function of yours, or a tool of your MCP servers): your app knows what your tools cost, and Vestra cannot. - `requires_present_user` (`boolean`): Whether a yes needs present_user: true, meaning the person who saw it said yes: true for a payment, and for an action approved in person. - `nonce` (`string`, may be absent): For an action approved in person (hands_approval) only: send it back with your yes. - `questions` (`object[]`, may be absent): For a question or a hand-off: what Bash asks, one or more questions. - `question` (`string`): The question, as Bash asks it. Example: `Which flight should I book?`. - `header` (`string`): A short label for it, for a heading or a chip; empty when Bash gave none. - `options` (`object[]`): Answers Bash suggests; your user may still answer in their own words. Empty when it suggests none. - `multi_select` (`boolean`): Whether more than one option may be picked. - `frame_url` (`string`, may be absent): For a hand-off, on a single read only (never in lists): the page Bash was on when it asked, as a JPEG data URI. - `browser` (`object`, may be absent): For a hand-off in the browser: open a session with a screen (POST /v1/sessions) so a person can do it. - `suggested_mode` (`"control"`): Open the screen with control, so the person can take the wheel. - `url` (`string | null`): The page Bash's browser is on; null when it did not say. - `details` (`map of any`): A few named facts for its kind, safe to show: such as tool_name and rows for a tool step (with server for a tool of an MCP server, and its arguments as rows), or app for a connection. Never the raw request. - `outcome` (`object | null`): How it ended; null while it is pending. - `decision` (`"approved" | "denied" | "answered" | "cancelled" | "expired"`): How it ended. - `answers` (`string[]`, may be absent): For an answered question: the answer to each question, in order. - `reason` (`string`, may be absent): For a denial: why, when one was recorded. - `message` (`string`, may be absent): For a denial: the note that was sent with the no, when there was one. - `created_at` (`string`): When Bash asked, as an ISO 8601 time. - `expires_at` (`string | null`): When it expires unanswered, as an ISO 8601 time; null when it never does. - `resolved_at` (`string | null`): When it was answered, withdrawn or expired, as an ISO 8601 time; null while pending. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const waiting = await vestra.answer.list(); ``` **Python** ```python waiting = vestra.answer.list() ``` **CLI** ```bash vestra answer ``` **HTTP** ```bash # List what Bash is waiting on. For one user, this lists approvals, questions, hand-offs, apps to connect and # calls to your tools. curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Approve or deny. A payment or hands-on approval needs present_user: true, and a hands-on approval also needs # its nonce. To say no, send approve: false. 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 }' # Answer a question. Send one answer per question. curl "$VESTRA_BASE_URL/v1/interactions/int_7qp/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "answers": ["The 09:15 flight"] }' # Return your tool's result. Answer a call to one of your tools with its output, or an error. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "status": "confirmed", "pnr": "100042" } }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "answer", "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true } } } ``` --- url: https://vestra.ai/developers/reference/api/interactions/get-v1-interactions-interaction-id.md --- # GET /v1/interactions/{interaction_id} > One item, by its int_ or call_ id: its type (approval, question, connect, handoff, tool_call), what Bash wants to do, the options, when it expires. In [Interactions API](https://vestra.ai/developers/reference/api/interactions.md): What Bash waits on (approvals, questions, apps to connect, hand-offs, calls to your tools) and the answers. Send `Authorization: Bearer vk_test_...` on every call. One item, by its int_ or call_ id: its type (approval, question, connect, handoff, tool_call), what Bash wants to do, the options, when it expires. A handoff carries a snapshot of the page. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`answer`](https://vestra.ai/developers/answer.md). Every call here is a checked sample. ## Request Path: - `interaction_id` (`string`, required): What Bash waits on, by its id: `int_...`, or `call_...` for a call to your tool. ## Answers 200 - `object` (`"interaction"`): Always interaction. - `id` (`string`): The interaction, to answer by its id (int_...; call_... for a call to your tool). Example: `int_3j9`. - `user_id` (`string | null`): Your id for the user it is for; null on your company's own Bash. - `task_id` (`string | null`): The task that asked (task_...); null for a call to your tool that Bash's reply made. - `conversation_id` (`string | null`): Its conversation (conv_...); null when its task has none. - `kind` (`"ask_question" | "composio_auth" | "tool_permission" | "credit_approval" | "exit_plan" | "custom" | "hands_approval" | "plan_limit"`): What exactly Bash waits on. ask_question: a question, or a hand-off. composio_auth: connecting an app. tool_permission: a step with a tool. credit_approval: spending on a paid tool. exit_plan: a plan before it starts. hands_approval: an action on a computer, approved in person. plan_limit: a plan's limit ran out (read only). custom: anything else, named by details.renderer. - `type` (`"approval" | "question" | "connect" | "handoff" | "tool_call"`): What kind of answer it takes. approval: yes or no. question: an answer to each question. connect: your user connects an app. handoff: a person does one thing, then says so. tool_call: your tool returns an output or an error. - `status` (`"pending" | "resolved" | "cancelled" | "expired"`): pending: Bash waits on it. resolved: it was answered. cancelled: it was withdrawn before an answer. expired: nobody answered in time. - `summary` (`string`): What Bash wants, in a phrase to show your user. Example: `pay, amount $500.00`. - `moves_money` (`boolean | null`): Whether saying yes spends money, such as a payment. null on the card for one of your own tools (a function of yours, or a tool of your MCP servers): your app knows what your tools cost, and Vestra cannot. - `requires_present_user` (`boolean`): Whether a yes needs present_user: true, meaning the person who saw it said yes: true for a payment, and for an action approved in person. - `nonce` (`string`, may be absent): For an action approved in person (hands_approval) only: send it back with your yes. - `questions` (`object[]`, may be absent): For a question or a hand-off: what Bash asks, one or more questions. - `question` (`string`): The question, as Bash asks it. Example: `Which flight should I book?`. - `header` (`string`): A short label for it, for a heading or a chip; empty when Bash gave none. - `options` (`object[]`): Answers Bash suggests; your user may still answer in their own words. Empty when it suggests none. - `label` (`string`): An answer your user can pick. - `description` (`string`): A line more about that answer; empty when there is none. - `multi_select` (`boolean`): Whether more than one option may be picked. - `frame_url` (`string`, may be absent): For a hand-off, on a single read only (never in lists): the page Bash was on when it asked, as a JPEG data URI. - `browser` (`object`, may be absent): For a hand-off in the browser: open a session with a screen (POST /v1/sessions) so a person can do it. - `suggested_mode` (`"control"`): Open the screen with control, so the person can take the wheel. - `url` (`string | null`): The page Bash's browser is on; null when it did not say. - `details` (`map of any`): A few named facts for its kind, safe to show: such as tool_name and rows for a tool step (with server for a tool of an MCP server, and its arguments as rows), or app for a connection. Never the raw request. - `outcome` (`object | null`): How it ended; null while it is pending. - `decision` (`"approved" | "denied" | "answered" | "cancelled" | "expired"`): How it ended. - `answers` (`string[]`, may be absent): For an answered question: the answer to each question, in order. - `reason` (`string`, may be absent): For a denial: why, when one was recorded. - `message` (`string`, may be absent): For a denial: the note that was sent with the no, when there was one. - `created_at` (`string`): When Bash asked, as an ISO 8601 time. - `expires_at` (`string | null`): When it expires unanswered, as an ISO 8601 time; null when it never does. - `resolved_at` (`string | null`): When it was answered, withdrawn or expired, as an ISO 8601 time; null while pending. ## Code **TypeScript** ```ts const interaction = await vestra.answer.get('int_3j9'); ``` **Python** ```python interaction = vestra.answer.get("int_3j9") ``` **HTTP** ```bash # interaction.created names what Bash waits on. Read it, show it to your user, then answer it. curl "$VESTRA_BASE_URL/v1/interactions/int_3j9" \ -H "Authorization: Bearer $VESTRA_API_KEY" 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 }' ``` **CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-answer.md --- # POST /v1/interactions/{interaction_id}/answer > Answer it, whatever it is. In [Interactions API](https://vestra.ai/developers/reference/api/interactions.md): What Bash waits on (approvals, questions, apps to connect, hand-offs, calls to your tools) and the answers. Send `Authorization: Bearer vk_test_...` on every call. Answer it, whatever it is. An approval takes approve (true or false), a question answers or approve: false, a hand-off done, could_not or not_now (or answers, when it asks for a value such as a code), a client tool call output or error. A money or account approval, and the yes to a company action (a tool of your MCP servers, or a function of yours that is not read-only or run without asking), must carry present_user: true. A body that fits another kind is 400 answer_mismatch. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`answer`](https://vestra.ai/developers/answer.md). Every call here is a checked sample. ## Request Path: - `interaction_id` (`string`, required): What Bash waits on, by its id: `int_...`, or `call_...` for a call to your tool. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: Either `{ approve, present_user, message, nonce }`: - `approve` (`boolean`, required): Yes or no to an approval. For a connection, only no: your user connects the app with POST /v1/connections, and the item settles itself. false also skips a question. - `present_user` (`boolean`): true when the person who saw it said yes. A yes that moves money, or is approved in person, needs it. - `message` (`string`): With a no: a note for Bash, in at most 500 characters. Limits: 1 to 500 characters. - `nonce` (`string`): For an action approved in person: the interaction's nonce, sent back. Limits: 1 to 4096 characters. Or `{ answers }`: - `answers` (`string[]`, required): One answer per question, in order: at most 4, each at most 4,000 characters. Limits: 1 to 4 items. Or `{ done }`: - `done` (`true`, required): For a hand-off: the person did it. Or `{ could_not }`: - `could_not` (`true`, required): For a hand-off: the person tried and could not. Or `{ not_now }`: - `not_now` (`true`, required): For a hand-off: not now; Bash carries on without it. Or `{ output }`: - `output` (`any`, required): For a call to your tool: what it returned, as any JSON. Or `{ error }`: - `error` (`string`, required): For a call to your tool: why it failed, in at most 4,000 characters. Limits: 1 to 4000 characters. ## Answers 200 - `id` (`string`): What you answered (int_... or call_...). - `object` (`"interaction"`): Always interaction. - `type` (`"approval" | "question" | "connect" | "handoff" | "tool_call"`): What kind of answer it takes. approval: yes or no. question: an answer to each question. connect: your user connects an app. handoff: a person does one thing, then says so. tool_call: your tool returns an output or an error. - `status` (`"resolved" | "completed" | "failed"`): resolved: Bash has your answer and carries on. For a call to your tool: completed with your output, or failed with your error. ## Code **TypeScript** ```ts await vestra.answer('int_3j9', { approve: true, present_user: true }); ``` **Python** ```python vestra.answer("int_3j9", approve=True, present_user=True) ``` **CLI** ```bash vestra answer int_3j9 --approve --present-user ``` **HTTP** ```bash # List what Bash is waiting on. For one user, this lists approvals, questions, hand-offs, apps to connect and # calls to your tools. curl "$VESTRA_BASE_URL/v1/interactions?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Approve or deny. A payment or hands-on approval needs present_user: true, and a hands-on approval also needs # its nonce. To say no, send approve: false. 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 }' # Answer a question. Send one answer per question. curl "$VESTRA_BASE_URL/v1/interactions/int_7qp/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "answers": ["The 09:15 flight"] }' # Return your tool's result. Answer a call to one of your tools with its output, or an error. curl "$VESTRA_BASE_URL/v1/interactions/call_4mz/answer" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "output": { "status": "confirmed", "pnr": "100042" } }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "answer", "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true } } } ``` --- url: https://vestra.ai/developers/reference/api/interactions/post-v1-interactions-interaction-id-cancel.md --- # POST /v1/interactions/{interaction_id}/cancel > Withdraw it. In [Interactions API](https://vestra.ai/developers/reference/api/interactions.md): What Bash waits on (approvals, questions, apps to connect, hand-offs, calls to your tools) and the answers. Send `Authorization: Bearer vk_test_...` on every call. Withdraw it. The task carries on without that action. Your API key, scope `runtime` or `all`. On the card: [`answer`](https://vestra.ai/developers/answer.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `interaction_id` (`string`, required): What Bash waits on, by its id: `int_...`, or `call_...` for a call to your tool. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, if you send one: - `reason` (`string`): Why it was withdrawn, for Bash, in at most 500 characters. Default: Withdrawn by the company. Default: `Withdrawn by the company.`. Limits: 1 to 500 characters. ## Answers 200 - `object` (`"interaction"`): Always interaction. - `id` (`string`): The interaction, to answer by its id (int_...; call_... for a call to your tool). Example: `int_3j9`. - `user_id` (`string | null`): Your id for the user it is for; null on your company's own Bash. - `task_id` (`string | null`): The task that asked (task_...); null for a call to your tool that Bash's reply made. - `conversation_id` (`string | null`): Its conversation (conv_...); null when its task has none. - `kind` (`"ask_question" | "composio_auth" | "tool_permission" | "credit_approval" | "exit_plan" | "custom" | "hands_approval" | "plan_limit"`): What exactly Bash waits on. ask_question: a question, or a hand-off. composio_auth: connecting an app. tool_permission: a step with a tool. credit_approval: spending on a paid tool. exit_plan: a plan before it starts. hands_approval: an action on a computer, approved in person. plan_limit: a plan's limit ran out (read only). custom: anything else, named by details.renderer. - `type` (`"approval" | "question" | "connect" | "handoff" | "tool_call"`): What kind of answer it takes. approval: yes or no. question: an answer to each question. connect: your user connects an app. handoff: a person does one thing, then says so. tool_call: your tool returns an output or an error. - `status` (`"pending" | "resolved" | "cancelled" | "expired"`): pending: Bash waits on it. resolved: it was answered. cancelled: it was withdrawn before an answer. expired: nobody answered in time. - `summary` (`string`): What Bash wants, in a phrase to show your user. Example: `pay, amount $500.00`. - `moves_money` (`boolean | null`): Whether saying yes spends money, such as a payment. null on the card for one of your own tools (a function of yours, or a tool of your MCP servers): your app knows what your tools cost, and Vestra cannot. - `requires_present_user` (`boolean`): Whether a yes needs present_user: true, meaning the person who saw it said yes: true for a payment, and for an action approved in person. - `nonce` (`string`, may be absent): For an action approved in person (hands_approval) only: send it back with your yes. - `questions` (`object[]`, may be absent): For a question or a hand-off: what Bash asks, one or more questions. - `question` (`string`): The question, as Bash asks it. Example: `Which flight should I book?`. - `header` (`string`): A short label for it, for a heading or a chip; empty when Bash gave none. - `options` (`object[]`): Answers Bash suggests; your user may still answer in their own words. Empty when it suggests none. - `label` (`string`): An answer your user can pick. - `description` (`string`): A line more about that answer; empty when there is none. - `multi_select` (`boolean`): Whether more than one option may be picked. - `frame_url` (`string`, may be absent): For a hand-off, on a single read only (never in lists): the page Bash was on when it asked, as a JPEG data URI. - `browser` (`object`, may be absent): For a hand-off in the browser: open a session with a screen (POST /v1/sessions) so a person can do it. - `suggested_mode` (`"control"`): Open the screen with control, so the person can take the wheel. - `url` (`string | null`): The page Bash's browser is on; null when it did not say. - `details` (`map of any`): A few named facts for its kind, safe to show: such as tool_name and rows for a tool step (with server for a tool of an MCP server, and its arguments as rows), or app for a connection. Never the raw request. - `outcome` (`object | null`): How it ended; null while it is pending. - `decision` (`"approved" | "denied" | "answered" | "cancelled" | "expired"`): How it ended. - `answers` (`string[]`, may be absent): For an answered question: the answer to each question, in order. - `reason` (`string`, may be absent): For a denial: why, when one was recorded. - `message` (`string`, may be absent): For a denial: the note that was sent with the no, when there was one. - `created_at` (`string`): When Bash asked, as an ISO 8601 time. - `expires_at` (`string | null`): When it expires unanswered, as an ISO 8601 time; null when it never does. - `resolved_at` (`string | null`): When it was answered, withdrawn or expired, as an ISO 8601 time; null while pending. ## Code **TypeScript** ```ts await vestra.answer.cancel('int_3j9'); ``` **Python** ```python vestra.answer.cancel("int_3j9") ``` **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/interactions/{interaction_id}/cancel" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/files.md --- # Files API > Files Bash makes, and files you hand it. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/files](https://vestra.ai/developers/reference/api/files/post-v1-files.md): Upload a file for Bash to use: multipart/form-data with one part named file. - `GET` [/v1/files](https://vestra.ai/developers/reference/api/files/get-v1-files.md): Files, each with its task and conversation: one user's (user_id) or the company's own Bash's, one task's outputs (task_id), or one conversation's, its tasks' outputs (conversation_id). - `GET` [/v1/files/{file_id}](https://vestra.ai/developers/reference/api/files/get-v1-files-file-id.md): Details and a short-lived download link. - `DELETE` [/v1/files/{file_id}](https://vestra.ai/developers/reference/api/files/delete-v1-files-file-id.md): Delete it. --- url: https://vestra.ai/developers/reference/api/files/post-v1-files.md --- # POST /v1/files > Upload a file for Bash to use: multipart/form-data with one part named file. In [Files API](https://vestra.ai/developers/reference/api/files.md): Files Bash makes, and files you hand it. Send `Authorization: Bearer vk_test_...` on every call. Upload a file for Bash to use: multipart/form-data with one part named file. user_id in the query names whose Bash it is for; without it, the company's own. Your API key, scope `runtime` or `all`. On the card: [`files`](https://vestra.ai/developers/files.md). Every call here is a checked sample. ## Request Query: - `user_id` (`string`): Whose file it is, by your id for them. Absent: your company's own Bash's. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, as a form: - `file` (`string`, required): The file, in a form field named file: 10 MB at most. Limits: at most 10485760 characters. ## Answers 201 - `id` (`string`): The file (file_...): hand it to an ask as an attachment. Example: `file_2nq`. - `object` (`"file"`): Always file. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `filename` (`string | null`): The file's name; null when it has none. Example: `itinerary.pdf`. - `mime_type` (`string | null`): Its type, such as application/pdf; null when unknown. - `size_bytes` (`integer | null`): Its size, in bytes; null when unknown. - `source` (`"upload" | "output"`): upload: a file you handed Bash. output: a file Bash made. - `task_id` (`string | null`): The task it belongs to (task_...): the one that made it, or used it; null when none did. - `conversation_id` (`string | null`): The conversation of its task (conv_...); null for a file no task touched. - `created_at` (`string`): When it was made or uploaded, as an ISO 8601 time. ## Code **TypeScript** ```ts const file = await vestra.user('traveller-42').files.upload('ticket.pdf'); ``` **Python** ```python file = vestra.user("traveller-42").files.upload("ticket.pdf") ``` **CLI** ```bash vestra files add ticket.pdf --user traveller-42 ``` **HTTP** ```bash # Upload a file. Send it for one user as multipart form data, with one part named file, up to 10 MB. curl "$VESTRA_BASE_URL/v1/files?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -F "file=@ticket.pdf" # Download what Bash made. List one conversation's files (its tasks' outputs), or one task's. curl "$VESTRA_BASE_URL/v1/files?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get one file. The response has a download link that works for a short while. curl "$VESTRA_BASE_URL/v1/files/file_2kq" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "files", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` --- url: https://vestra.ai/developers/reference/api/files/get-v1-files.md --- # GET /v1/files > Files, each with its task and conversation: one user's (user_id) or the company's own Bash's, one task's outputs (task_id), or one conversation's, its tasks' outputs (conversation_id). In [Files API](https://vestra.ai/developers/reference/api/files.md): Files Bash makes, and files you hand it. Send `Authorization: Bearer vk_test_...` on every call. Files, each with its task and conversation: one user's (user_id) or the company's own Bash's, one task's outputs (task_id), or one conversation's, its tasks' outputs (conversation_id). source picks what you handed Bash (upload) or what Bash made (output). Your API key, scope `runtime` or `all`. On the card: [`files`](https://vestra.ai/developers/files.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): One user's files, by your id for them. Absent: your company's own Bash's. - `task_id` (`string`): What one task made (task_...): outputs only. - `conversation_id` (`string`): One conversation's files, from all its tasks (conv_...). - `source` (`"upload" | "output"`): Only uploads or only outputs. Default: outputs with task_id or conversation_id, uploads without. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The file (file_...): hand it to an ask as an attachment. Example: `file_2nq`. - `object` (`"file"`): Always file. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `filename` (`string | null`): The file's name; null when it has none. Example: `itinerary.pdf`. - `mime_type` (`string | null`): Its type, such as application/pdf; null when unknown. - `size_bytes` (`integer | null`): Its size, in bytes; null when unknown. - `source` (`"upload" | "output"`): upload: a file you handed Bash. output: a file Bash made. - `task_id` (`string | null`): The task it belongs to (task_...): the one that made it, or used it; null when none did. - `conversation_id` (`string | null`): The conversation of its task (conv_...); null for a file no task touched. - `created_at` (`string`): When it was made or uploaded, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const files = await vestra.files.list(); ``` **Python** ```python files = vestra.files.list() ``` **CLI** ```bash vestra files ``` **HTTP** ```bash # Upload a file. Send it for one user as multipart form data, with one part named file, up to 10 MB. curl "$VESTRA_BASE_URL/v1/files?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -F "file=@ticket.pdf" # Download what Bash made. List one conversation's files (its tasks' outputs), or one task's. curl "$VESTRA_BASE_URL/v1/files?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get one file. The response has a download link that works for a short while. curl "$VESTRA_BASE_URL/v1/files/file_2kq" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "files", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` --- url: https://vestra.ai/developers/reference/api/files/get-v1-files-file-id.md --- # GET /v1/files/{file_id} > Details and a short-lived download link. In [Files API](https://vestra.ai/developers/reference/api/files.md): Files Bash makes, and files you hand it. Send `Authorization: Bearer vk_test_...` on every call. Details and a short-lived download link. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`files`](https://vestra.ai/developers/files.md). Every call here is a checked sample. ## Request Path: - `file_id` (`string`, required): The file's id (`file_...`). ## Answers 200 - `id` (`string`): The file (file_...): hand it to an ask as an attachment. Example: `file_2nq`. - `object` (`"file"`): Always file. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `filename` (`string | null`): The file's name; null when it has none. Example: `itinerary.pdf`. - `mime_type` (`string | null`): Its type, such as application/pdf; null when unknown. - `size_bytes` (`integer | null`): Its size, in bytes; null when unknown. - `source` (`"upload" | "output"`): upload: a file you handed Bash. output: a file Bash made. - `task_id` (`string | null`): The task it belongs to (task_...): the one that made it, or used it; null when none did. - `conversation_id` (`string | null`): The conversation of its task (conv_...); null for a file no task touched. - `created_at` (`string`): When it was made or uploaded, as an ISO 8601 time. - `download_url` (`string (URL)`): A link to download it: good for 15 minutes, so read the file again for a fresh one. - `download_expires_at` (`string`): When the link stops working, as an ISO 8601 time. ## Code **TypeScript** ```ts const file = await vestra.files.get('file_2kq'); ``` **Python** ```python file = vestra.files.get("file_2kq") ``` **CLI** ```bash vestra files get file_2kq ``` **HTTP** ```bash # Upload a file. Send it for one user as multipart form data, with one part named file, up to 10 MB. curl "$VESTRA_BASE_URL/v1/files?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -F "file=@ticket.pdf" # Download what Bash made. List one conversation's files (its tasks' outputs), or one task's. curl "$VESTRA_BASE_URL/v1/files?conversation_id=conv_8f2k" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Get one file. The response has a download link that works for a short while. curl "$VESTRA_BASE_URL/v1/files/file_2kq" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "files", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` --- url: https://vestra.ai/developers/reference/api/files/delete-v1-files-file-id.md --- # DELETE /v1/files/{file_id} > Delete it. In [Files API](https://vestra.ai/developers/reference/api/files.md): Files Bash makes, and files you hand it. Send `Authorization: Bearer vk_test_...` on every call. Delete it. Your API key, scope `runtime` or `all`. On the card: [`files`](https://vestra.ai/developers/files.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `file_id` (`string`, required): The file's id (`file_...`). ## Answers 200 - `id` (`string`): The file (file_...). - `object` (`"file"`): Always file. - `deleted` (`true`): Always true: it is gone. ## Code **TypeScript** ```ts await vestra.files.delete('file_2kq'); ``` **Python** ```python vestra.files.delete("file_2kq") ``` **CLI** ```bash vestra files delete file_2kq ``` **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/files/{file_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/connected-apps.md --- # Connected apps API > The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/apps](https://vestra.ai/developers/reference/api/connected-apps/get-v1-apps.md): The apps your users may connect, after your allowlist. - `POST` [/v1/connections](https://vestra.ai/developers/reference/api/connected-apps/post-v1-connections.md): Get a connect link for one app, for one user (user_id) or the company's own Bash. - `GET` [/v1/connections](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections.md): The connected apps of one user (user_id), or of the company's own Bash. - `GET` [/v1/connections/{connection_id}](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections-connection-id.md): One connection's status. - `DELETE` [/v1/connections/{connection_id}](https://vestra.ai/developers/reference/api/connected-apps/delete-v1-connections-connection-id.md): Disconnect. - `GET` [/v1/connect/return](https://vestra.ai/developers/reference/api/connected-apps/get-v1-connect-return.md): Hosted by us. --- url: https://vestra.ai/developers/reference/api/connected-apps/get-v1-apps.md --- # GET /v1/apps > The apps your users may connect, after your allowlist. In [Connected apps API](https://vestra.ai/developers/reference/api/connected-apps.md): The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. The apps your users may connect, after your allowlist. Your API key, scope `runtime` or `all`. On the card: [`connect`](https://vestra.ai/developers/connect.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `search` (`string`): Only apps whose name holds these words. - `category` (`string`): Only apps of this category. - `deployment_id` (`string`): Only the apps this deployment allows (its `apps`), by its `dep_` id or name. Absent: the company default's. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"app"`): Always `app`. - `slug` (`string`): The app's name in calls: what `app` takes when you connect it. Example: `gmail`. - `name` (`string`): The app's name, as people read it. Example: `Gmail`. - `description` (`string`): What the app is, in a line. - `category` (`string | null`): What kind of app it is, such as `email`; null when it has none. - `logo_url` (`string`): The app's logo, an image URL. - `connectable` (`boolean`): false when Vestra cannot connect it yet. - `no_auth` (`boolean`): true when it needs no sign-in: its tools just run. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const catalog = await vestra.connect.catalog(); ``` **Python** ```python catalog = vestra.connect.catalog() ``` **CLI** ```bash vestra connect --catalog ``` **HTTP** ```bash # Get a sign-in link for one app. It is for one user: send them its url. curl "$VESTRA_BASE_URL/v1/connections" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "app": "gmail" }' # List their apps. You can also list the catalog your users may pick from. curl "$VESTRA_BASE_URL/v1/connections?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/apps?search=calendar" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Disconnect an app. user_id says whose connection it is. curl -X DELETE "$VESTRA_BASE_URL/v1/connections/con_7hs?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "connect", "arguments": { "user": "traveller-42", "app": "gmail" } } } ``` --- url: https://vestra.ai/developers/reference/api/connected-apps/post-v1-connections.md --- # POST /v1/connections > Get a connect link for one app, for one user (user_id) or the company's own Bash. In [Connected apps API](https://vestra.ai/developers/reference/api/connected-apps.md): The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. Get a connect link for one app, for one user (user_id) or the company's own Bash. After consent the user lands on your return_url (or your first allowed one, or our own page), and a webhook fires. The link carries the id of the connection it makes. Your API key, scope `runtime` or `all`. On the card: [`connect`](https://vestra.ai/developers/connect.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `user_id` (`string`): Whose Bash: your id for the user. Absent: your company's own Bash. Example: `traveller-42`. - `app` (`string`, required): The app to connect, by its slug from GET /v1/apps. Example: `gmail`. - `return_url` (`string (URL)`): Where the user lands after they sign in; it must match one of your allowed return URLs. Absent: your first allowed return URL, or our own page. Limits: at most 2048 characters. Example: `https://app.example.com/connected`. - `deployment_id` (`string`): The deployment whose `apps` decide whether this app is allowed, by its `dep_` id or name. Absent: the company default's. Limits: 1 to 128 characters. ## Answers 200 or 201 - `object` (`"connect_link"`): Always `connect_link`. - `id` (`string | null`): The connection this link makes (`con_`), to read with GET /v1/connections/{connection_id}; null only when there is none to make (an app with no sign-in). - `status` (`"pending" | "connected"`): `pending`: send the user to `url`. `connected`: they already had this app, or it needs no sign-in. - `url` (`string | null`): The sign-in link: send it to your user, or open it yourself for your company's own Bash. Null when there is nothing to sign in to. - `connection` (`object | null`): The connection when it is already made; null while it is pending. - `object` (`"connection"`): Always `connection`. - `id` (`string`): The connection's id. Example: `con_8f2k`. - `user_id` (`string | null`): Your id for the user; null for your company's own Bash. - `app` (`string`): The app, by its slug. Example: `gmail`. - `status` (`"active" | "pending" | "initializing" | "expired" | "revoked" | "inactive" | "failed"`): `active`: Bash can use it. `pending` or `initializing`: the sign-in is not finished. `expired`, `revoked`, `inactive` or `failed`: it no longer works, and the user connects it again. - `label` (`string | null`): The account's label, usually its email; null when the app gives none. - `created_at` (`string`): When it was connected, as an ISO 8601 date and time in UTC. - `expires_at` (`string`): When the link stops working, as an ISO 8601 date and time in UTC. ## Code **TypeScript** ```ts const link = await vestra.user('traveller-42').connect('gmail'); ``` **Python** ```python link = vestra.user("traveller-42").connect("gmail") ``` **CLI** ```bash vestra connect gmail --user traveller-42 ``` **HTTP** ```bash # Get a sign-in link for one app. It is for one user: send them its url. curl "$VESTRA_BASE_URL/v1/connections" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "app": "gmail" }' # List their apps. You can also list the catalog your users may pick from. curl "$VESTRA_BASE_URL/v1/connections?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/apps?search=calendar" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Disconnect an app. user_id says whose connection it is. curl -X DELETE "$VESTRA_BASE_URL/v1/connections/con_7hs?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "connect", "arguments": { "user": "traveller-42", "app": "gmail" } } } ``` --- url: https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections.md --- # GET /v1/connections > The connected apps of one user (user_id), or of the company's own Bash. In [Connected apps API](https://vestra.ai/developers/reference/api/connected-apps.md): The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. The connected apps of one user (user_id), or of the company's own Bash. Your API key, scope `runtime` or `all`. On the card: [`connect`](https://vestra.ai/developers/connect.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): Whose Bash: your id for the user. Absent: your company's own Bash. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"connection"`): Always `connection`. - `id` (`string`): The connection's id. Example: `con_8f2k`. - `user_id` (`string | null`): Your id for the user; null for your company's own Bash. - `app` (`string`): The app, by its slug. Example: `gmail`. - `status` (`"active" | "pending" | "initializing" | "expired" | "revoked" | "inactive" | "failed"`): `active`: Bash can use it. `pending` or `initializing`: the sign-in is not finished. `expired`, `revoked`, `inactive` or `failed`: it no longer works, and the user connects it again. - `label` (`string | null`): The account's label, usually its email; null when the app gives none. - `created_at` (`string`): When it was connected, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const connections = await vestra.connect.list(); ``` **Python** ```python connections = vestra.connect.list() ``` **CLI** ```bash vestra connect ``` **HTTP** ```bash # Get a sign-in link for one app. It is for one user: send them its url. curl "$VESTRA_BASE_URL/v1/connections" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "app": "gmail" }' # List their apps. You can also list the catalog your users may pick from. curl "$VESTRA_BASE_URL/v1/connections?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/apps?search=calendar" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Disconnect an app. user_id says whose connection it is. curl -X DELETE "$VESTRA_BASE_URL/v1/connections/con_7hs?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "connect", "arguments": { "user": "traveller-42", "app": "gmail" } } } ``` --- url: https://vestra.ai/developers/reference/api/connected-apps/get-v1-connections-connection-id.md --- # GET /v1/connections/{connection_id} > One connection's status. In [Connected apps API](https://vestra.ai/developers/reference/api/connected-apps.md): The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. One connection's status. user_id names whose it is. Your API key, scope `runtime` or `all`. On the card: [`connect`](https://vestra.ai/developers/connect.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `connection_id` (`string`, required): The connected app's id (`con_...`). Query: - `user_id` (`string`): Whose Bash: your id for the user. Absent: your company's own Bash. ## Answers 200 - `object` (`"connection"`): Always `connection`. - `id` (`string`): The connection's id. Example: `con_8f2k`. - `user_id` (`string | null`): Your id for the user; null for your company's own Bash. - `app` (`string`): The app, by its slug. Example: `gmail`. - `status` (`"active" | "pending" | "initializing" | "expired" | "revoked" | "inactive" | "failed"`): `active`: Bash can use it. `pending` or `initializing`: the sign-in is not finished. `expired`, `revoked`, `inactive` or `failed`: it no longer works, and the user connects it again. - `label` (`string | null`): The account's label, usually its email; null when the app gives none. - `created_at` (`string`): When it was connected, as an ISO 8601 date and time in UTC. ## Code **TypeScript** ```ts const connection = await vestra.user('traveller-42').connect.get('con_7hs'); ``` **Python** ```python connection = vestra.user("traveller-42").connect.get("con_7hs") ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/connections/{connection_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/connected-apps/delete-v1-connections-connection-id.md --- # DELETE /v1/connections/{connection_id} > Disconnect. In [Connected apps API](https://vestra.ai/developers/reference/api/connected-apps.md): The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. Disconnect. user_id names whose it is. Your API key, scope `runtime` or `all`. On the card: [`connect`](https://vestra.ai/developers/connect.md). Every call here is a checked sample. ## Request Path: - `connection_id` (`string`, required): The connected app's id (`con_...`). Query: - `user_id` (`string`): Whose Bash: your id for the user. Absent: your company's own Bash. ## Answers 200 - `object` (`"connection"`): Always `connection`. - `id` (`string`): The disconnected connection's id. - `deleted` (`true`): Always true: Bash can no longer use the app for this user. ## Code **TypeScript** ```ts await vestra.user('traveller-42').connect.remove('con_7hs'); ``` **Python** ```python vestra.user("traveller-42").connect.remove("con_7hs") ``` **CLI** ```bash vestra connect --remove con_7hs --user traveller-42 ``` **HTTP** ```bash # Get a sign-in link for one app. It is for one user: send them its url. curl "$VESTRA_BASE_URL/v1/connections" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "app": "gmail" }' # List their apps. You can also list the catalog your users may pick from. curl "$VESTRA_BASE_URL/v1/connections?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/apps?search=calendar" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Disconnect an app. user_id says whose connection it is. curl -X DELETE "$VESTRA_BASE_URL/v1/connections/con_7hs?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "connect", "arguments": { "user": "traveller-42", "app": "gmail" } } } ``` --- url: https://vestra.ai/developers/reference/api/connected-apps/get-v1-connect-return.md --- # GET /v1/connect/return > Hosted by us. In [Connected apps API](https://vestra.ai/developers/reference/api/connected-apps.md): The apps each user connects through a sign-in link, and the apps they may choose from. Send `Authorization: Bearer vk_test_...` on every call. Hosted by us. The app sends the user here after consent; we record it and forward them to your return_url. You never call it. No key: anyone may call it. On the card: [`connect`](https://vestra.ai/developers/connect.md). Over HTTP it is made from the contract: fill in every value. ## Request Nothing to send but the route. ## Answers 200 Its answer is a protocol of its own: its line's page says more. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/connect/return" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md --- # MCP servers and secrets API > Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/mcp-servers](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers.md): Your registered MCP servers. - `POST` [/v1/mcp-servers](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers.md): Register one: URL, auth (none, or headers using ${secret:NAME} or ${user_secret:NAME}), and its tool policy floor: tools never run and tools that always need approval, for every deployment in both modes. - `GET` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id.md): One server and its settings. - `PATCH` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/patch-v1-mcp-servers-server-id.md): Change URL, auth or the tool policy floor, for every deployment in both modes. - `DELETE` [/v1/mcp-servers/{server_id}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-mcp-servers-server-id.md): Remove it from every user. - `POST` [/v1/mcp-servers/{server_id}/check](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers-server-id-check.md): Connect now and list its tools, to prove it works. - `GET` [/v1/mcp-servers/{server_id}/tools](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id-tools.md): The tools we found, each with its setting in the server's tool policy floor. - `PUT` [/v1/secrets/{name}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/put-v1-secrets-name.md): Store a secret, write-only: the company's (refer to it as ${secret:NAME}), or with user_id one user's own (${user_secret:NAME}), for example their travel account token. - `DELETE` [/v1/secrets/{name}](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-secrets-name.md): Remove one: the company's, or one user's (user_id). - `GET` [/v1/secrets](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-secrets.md): Secret names, never the values: the company's, or one user's (user_id). --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers.md --- # GET /v1/mcp-servers > Your registered MCP servers. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Your registered MCP servers. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"mcp_server"`): Always `mcp_server`. - `id` (`string`): The MCP server's id. Example: `mcp_8f2k`. - `slug` (`string`): The name Bash calls it by. - `name` (`string`): Its name, as people read it. - `url` (`string`): The server's https URL. - `auth` (`object`): How Bash signs in to it. - `type` (`"none" | "headers"`): `none`, or `headers` sent with every call. - `headers` (`map of string`, may be absent): The headers, as written: a credential shows as its `${secret:NAME}`, never its value. - `tools` (`object`): Which of its tools Bash never calls (`deny`), and which wait for the person's yes (`require_approval`), in every deployment that uses the server. A deployment may add to either list, never take from it. - `deny` (`string[]`): Tools of the server Bash never calls, by name, at most 200. Every deployment that names the server runs under it. - `require_approval` (`string[]`): Tools of the server that always wait for the person's yes, by name, at most 200. - `secrets` (`string[]`): The company secrets it needs (`${secret:NAME}`), by name: store each with PUT /v1/secrets/{name}. - `user_secrets` (`string[]`): The secrets each user needs (`${user_secret:NAME}`), by name: store each user's with PUT /v1/secrets/{name} and their `user_id`. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/mcp-servers" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers.md --- # POST /v1/mcp-servers > Register one: URL, auth (none, or headers using ${secret:NAME} or ${user_secret:NAME}), and its tool policy floor: tools never run and tools that always need approval, for every deployment in both modes. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Register one: URL, auth (none, or headers using ${secret:NAME} or ${user_secret:NAME}), and its tool policy floor: tools never run and tools that always need approval, for every deployment in both modes. A deployment version's own tool policy adds to the floor and never removes from it. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `name` (`string`, required): Its name, as people read it, 1 to 80 characters. Limits: 1 to 80 characters. Example: `Travel desk`. - `slug` (`string`, required): The name Bash calls it by: lowercase letters, digits and hyphens, starting with a letter, up to 40. Example: `travel-desk`. - `url` (`string (URL)`, required): The server's public https URL, up to 2,048 characters. Limits: at most 2048 characters. Example: `https://mcp.example.com/travel`. - `auth` (`object`): How Bash signs in to it. Default `{ "type": "none" }`. Default: `{"type":"none"}`. One of: `{ type }`, `{ type, headers }`, `{ type }`. - `tools` (`object`): Which of its tools Bash never calls (`deny`), and which wait for the person's yes (`require_approval`), in every deployment that uses the server. A deployment may add to either list, never take from it. Default: no rule. Default: `{"deny":[],"require_approval":[]}`. - `deny` (`string[]`): Tools of the server Bash never calls, by name, at most 200. Every deployment that names the server runs under it. Default: `[]`. Limits: at most 200 items. - `require_approval` (`string[]`): Tools of the server that always wait for the person's yes, by name, at most 200. Default: `[]`. Limits: at most 200 items. ## Answers 201 - `object` (`"mcp_server"`): Always `mcp_server`. - `id` (`string`): The MCP server's id. Example: `mcp_8f2k`. - `slug` (`string`): The name Bash calls it by. - `name` (`string`): Its name, as people read it. - `url` (`string`): The server's https URL. - `auth` (`object`): How Bash signs in to it. - `type` (`"none" | "headers"`): `none`, or `headers` sent with every call. - `headers` (`map of string`, may be absent): The headers, as written: a credential shows as its `${secret:NAME}`, never its value. - `tools` (`object`): Which of its tools Bash never calls (`deny`), and which wait for the person's yes (`require_approval`), in every deployment that uses the server. A deployment may add to either list, never take from it. - `deny` (`string[]`): Tools of the server Bash never calls, by name, at most 200. Every deployment that names the server runs under it. - `require_approval` (`string[]`): Tools of the server that always wait for the person's yes, by name, at most 200. - `secrets` (`string[]`): The company secrets it needs (`${secret:NAME}`), by name: store each with PUT /v1/secrets/{name}. - `user_secrets` (`string[]`): The secrets each user needs (`${user_secret:NAME}`), by name: store each user's with PUT /v1/secrets/{name} and their `user_id`. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/mcp-servers" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Travel desk", "slug": "travel-desk", "url": "https://mcp.example.com/travel" }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id.md --- # GET /v1/mcp-servers/{server_id} > One server and its settings. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. One server and its settings. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `server_id` (`string`, required): The MCP server's id (`mcp_...`). ## Answers 200 - `object` (`"mcp_server"`): Always `mcp_server`. - `id` (`string`): The MCP server's id. Example: `mcp_8f2k`. - `slug` (`string`): The name Bash calls it by. - `name` (`string`): Its name, as people read it. - `url` (`string`): The server's https URL. - `auth` (`object`): How Bash signs in to it. - `type` (`"none" | "headers"`): `none`, or `headers` sent with every call. - `headers` (`map of string`, may be absent): The headers, as written: a credential shows as its `${secret:NAME}`, never its value. - `tools` (`object`): Which of its tools Bash never calls (`deny`), and which wait for the person's yes (`require_approval`), in every deployment that uses the server. A deployment may add to either list, never take from it. - `deny` (`string[]`): Tools of the server Bash never calls, by name, at most 200. Every deployment that names the server runs under it. - `require_approval` (`string[]`): Tools of the server that always wait for the person's yes, by name, at most 200. - `secrets` (`string[]`): The company secrets it needs (`${secret:NAME}`), by name: store each with PUT /v1/secrets/{name}. - `user_secrets` (`string[]`): The secrets each user needs (`${user_secret:NAME}`), by name: store each user's with PUT /v1/secrets/{name} and their `user_id`. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/mcp-servers/{server_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/patch-v1-mcp-servers-server-id.md --- # PATCH /v1/mcp-servers/{server_id} > Change URL, auth or the tool policy floor, for every deployment in both modes. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Change URL, auth or the tool policy floor, for every deployment in both modes. Users pick it up at their next task. While a deployment's live version uses the server, a test key's change answers 409 mcp_server_live: change it with a live key, or as an owner or admin. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `server_id` (`string`, required): The MCP server's id (`mcp_...`). Body: - `name` (`string`): A new name, 1 to 80 characters. Limits: 1 to 80 characters. - `url` (`string (URL)`): A new public https URL. Limits: at most 2048 characters. - `auth` (`object`): How Bash signs in to it, from now on. One of: `{ type }`, `{ type, headers }`, `{ type }`. - `tools` (`object`): New rules for its tools (`deny`, `require_approval`), for every deployment that uses the server; a list left out stays as it is. - `deny` (`string[]`): Tools of the server Bash never calls, by name, at most 200. Every deployment that names the server runs under it. It replaces the list; left out, the list stays. Limits: at most 200 items. - `require_approval` (`string[]`): Tools of the server that always wait for the person's yes, by name, at most 200. It replaces the list; left out, the list stays. Limits: at most 200 items. ## Answers 200 - `object` (`"mcp_server"`): Always `mcp_server`. - `id` (`string`): The MCP server's id. Example: `mcp_8f2k`. - `slug` (`string`): The name Bash calls it by. - `name` (`string`): Its name, as people read it. - `url` (`string`): The server's https URL. - `auth` (`object`): How Bash signs in to it. - `type` (`"none" | "headers"`): `none`, or `headers` sent with every call. - `headers` (`map of string`, may be absent): The headers, as written: a credential shows as its `${secret:NAME}`, never its value. - `tools` (`object`): Which of its tools Bash never calls (`deny`), and which wait for the person's yes (`require_approval`), in every deployment that uses the server. A deployment may add to either list, never take from it. - `deny` (`string[]`): Tools of the server Bash never calls, by name, at most 200. Every deployment that names the server runs under it. - `require_approval` (`string[]`): Tools of the server that always wait for the person's yes, by name, at most 200. - `secrets` (`string[]`): The company secrets it needs (`${secret:NAME}`), by name: store each with PUT /v1/secrets/{name}. - `user_secrets` (`string[]`): The secrets each user needs (`${user_secret:NAME}`), by name: store each user's with PUT /v1/secrets/{name} and their `user_id`. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X PATCH "$VESTRA_BASE_URL/v1/mcp-servers/{server_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-mcp-servers-server-id.md --- # DELETE /v1/mcp-servers/{server_id} > Remove it from every user. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Remove it from every user. While a deployment's live version uses it, a test key answers 409 mcp_server_live. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `server_id` (`string`, required): The MCP server's id (`mcp_...`). ## Answers 200 - `object` (`"mcp_server"`): Always `mcp_server`. - `id` (`string`): The MCP server's id. Example: `mcp_8f2k`. - `deleted` (`true`): Always true: it is gone. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/mcp-servers/{server_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/post-v1-mcp-servers-server-id-check.md --- # POST /v1/mcp-servers/{server_id}/check > Connect now and list its tools, to prove it works. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Connect now and list its tools, to prove it works. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `server_id` (`string`, required): The MCP server's id (`mcp_...`). Body, if you send one: - `user_id` (`string`): Your id for a user, to fill in their `${user_secret:NAME}` headers. Absent: the company's own secrets only. Limits: 1 to 256 characters. Example: `traveller-42`. ## Answers 200 - `object` (`"mcp_check"`): Always `mcp_check`. - `ok` (`boolean`): Whether Bash reached the server and listed its tools. - `tools` (`string[]`): The tools it offers, by name; empty when it could not be reached. - `reason` (`"missing_secret" | "auth_required" | "url_not_public" | "unreachable"`, may be absent): Why not, when `ok` is false: `missing_secret` (see `missing`), `auth_required` (it asked for credentials), `url_not_public`, or `unreachable`. - `missing` (`string[]`, may be absent): With `missing_secret`, the secrets to store first, by name. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/mcp-servers/{server_id}/check" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-mcp-servers-server-id-tools.md --- # GET /v1/mcp-servers/{server_id}/tools > The tools we found, each with its setting in the server's tool policy floor. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. The tools we found, each with its setting in the server's tool policy floor. A deployment version's own policy can add to it. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `server_id` (`string`, required): The MCP server's id (`mcp_...`). Query: - `user_id` (`string`): Your id for a user, to fill in their `${user_secret:NAME}` headers. Absent: the company's own secrets only. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"mcp_tool"`): Always `mcp_tool`. - `name` (`string`): The tool's name, as the server gives it. - `denied` (`boolean`): true when the floor says Bash never calls it. - `requires_approval` (`boolean`): true when the floor says it waits for the person's yes. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/mcp-servers/{server_id}/tools" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/put-v1-secrets-name.md --- # PUT /v1/secrets/{name} > Store a secret, write-only: the company's (refer to it as ${secret:NAME}), or with user_id one user's own (${user_secret:NAME}), for example their travel account token. In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Store a secret, write-only: the company's (refer to it as ${secret:NAME}), or with user_id one user's own (${user_secret:NAME}), for example their travel account token. Storing it again replaces it. Your API key, scope `setup` or `all`. On the card: [`secrets`](https://vestra.ai/developers/secrets.md). Every call here is a checked sample. ## Request Path: - `name` (`string`, required): The secret's name: capitals, digits and underscores, starting with a capital. Body: - `user_id` (`string`): Whose secret: your id for the user, for their own (read as `${user_secret:NAME}`). Absent: your company's (read as `${secret:NAME}`). Example: `traveller-42`. - `value` (`string`, required): The secret itself, up to 8,192 characters. It is write-only: no call ever returns it. Limits: 1 to 8192 characters. ## Answers 200 - `object` (`"secret"`): Always `secret`. - `name` (`string`): The secret's name. Example: `PNR_API_KEY`. - `user_id` (`string | null`): Your id for the user whose secret it is; null for your company's. - `mode` (`"live" | "test"`): The mode it is kept in: a live key reads live secrets, a test key test ones. ## Code **TypeScript** ```ts await vestra.secrets.set('TRIPNEST_MCP_TOKEN', process.env.TRIPNEST_MCP_TOKEN!); ``` **Python** ```python vestra.secrets.set("TRIPNEST_MCP_TOKEN", os.environ["TRIPNEST_MCP_TOKEN"]) ``` **CLI** ```bash vestra secrets set TRIPNEST_MCP_TOKEN ``` **HTTP** ```bash # Store a company secret. Your MCP servers refer to it as ${secret:TRIPNEST_MCP_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "your-token" }' # Store a user's secret. It is for their own account, as ${user_secret:IRCTC_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/IRCTC_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "value": "token-from-their-account" }' # List and delete secrets. The list shows names only, never values. curl "$VESTRA_BASE_URL/v1/secrets" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/delete-v1-secrets-name.md --- # DELETE /v1/secrets/{name} > Remove one: the company's, or one user's (user_id). In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Remove one: the company's, or one user's (user_id). Your API key, scope `setup` or `all`. On the card: [`secrets`](https://vestra.ai/developers/secrets.md). Every call here is a checked sample. ## Request Path: - `name` (`string`, required): The secret's name: capitals, digits and underscores, starting with a capital. Query: - `user_id` (`string`): Whose secret: your id for the user, for their own (read as `${user_secret:NAME}`). Absent: your company's (read as `${secret:NAME}`). ## Answers 200 - `object` (`"secret"`): Always `secret`. - `name` (`string`): The secret's name. Example: `PNR_API_KEY`. - `user_id` (`string | null`): Your id for the user whose secret it is; null for your company's. - `deleted` (`true`): Always true: it is gone. ## Code **TypeScript** ```ts await vestra.secrets.delete('TRIPNEST_MCP_TOKEN'); ``` **Python** ```python vestra.secrets.delete("TRIPNEST_MCP_TOKEN") ``` **CLI** ```bash vestra secrets delete TRIPNEST_MCP_TOKEN ``` **HTTP** ```bash # Store a company secret. Your MCP servers refer to it as ${secret:TRIPNEST_MCP_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "your-token" }' # Store a user's secret. It is for their own account, as ${user_secret:IRCTC_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/IRCTC_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "value": "token-from-their-account" }' # List and delete secrets. The list shows names only, never values. curl "$VESTRA_BASE_URL/v1/secrets" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-servers-and-secrets/get-v1-secrets.md --- # GET /v1/secrets > Secret names, never the values: the company's, or one user's (user_id). In [MCP servers and secrets API](https://vestra.ai/developers/reference/api/mcp-servers-and-secrets.md): Your MCP servers and the tools they offer, and the secrets Bash uses but never shows. Send `Authorization: Bearer vk_test_...` on every call. Secret names, never the values: the company's, or one user's (user_id). Your API key, scope `setup` or `all`. On the card: [`secrets`](https://vestra.ai/developers/secrets.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): Whose secret: your id for the user, for their own (read as `${user_secret:NAME}`). Absent: your company's (read as `${secret:NAME}`). ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"secret"`): Always `secret`. - `name` (`string`): The secret's name. Example: `PNR_API_KEY`. - `user_id` (`string | null`): Your id for the user whose secret it is; null for your company's. - `mode` (`"live" | "test"`): The mode it is kept in: a live key reads live secrets, a test key test ones. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const secrets = await vestra.secrets.list(); ``` **Python** ```python secrets = vestra.secrets.list() ``` **CLI** ```bash vestra secrets ``` **HTTP** ```bash # Store a company secret. Your MCP servers refer to it as ${secret:TRIPNEST_MCP_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "your-token" }' # Store a user's secret. It is for their own account, as ${user_secret:IRCTC_TOKEN}. curl -X PUT "$VESTRA_BASE_URL/v1/secrets/IRCTC_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "value": "token-from-their-account" }' # List and delete secrets. The list shows names only, never values. curl "$VESTRA_BASE_URL/v1/secrets" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/secrets/TRIPNEST_MCP_TOKEN" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/skills.md --- # Skills API > Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/skills](https://vestra.ai/developers/reference/api/skills/get-v1-skills.md): Your company skills, plus the skills Bash already has. - `POST` [/v1/skills](https://vestra.ai/developers/reference/api/skills/post-v1-skills.md): Upload a skill folder as a zip, or as JSON files. - `GET` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id.md): One skill and its files. - `PUT` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/put-v1-skills-skill-id.md): Upload a new version. - `DELETE` [/v1/skills/{skill_id}](https://vestra.ai/developers/reference/api/skills/delete-v1-skills-skill-id.md): Remove it from every user. - `GET` [/v1/skills/{skill_id}/versions](https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id-versions.md): Every version, each with a content hash. --- url: https://vestra.ai/developers/reference/api/skills/get-v1-skills.md --- # GET /v1/skills > Your company skills, plus the skills Bash already has. In [Skills API](https://vestra.ai/developers/reference/api/skills.md): Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. Your company skills, plus the skills Bash already has. A company skill carries sha256, the hash of its files as uploaded, computed the way the SDKs compute a folder's, so an unchanged folder is never uploaded again. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `source` (`"company" | "built_in"`): `company` (the default): the skills you uploaded. `built_in`: the skills Bash already has. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"skill"`): Always `skill`. - `id` (`string | null`): The skill's id (`skl_`); null for a skill Bash already has. - `source` (`"company" | "built_in"`): `company`: one you uploaded. `built_in`: one Bash already has. - `slug` (`string`): Its name in a deployment: what `skills` takes for one Bash already has. - `name` (`string`): Its name, from its SKILL.md. - `description` (`string`): What it does, from its SKILL.md: Bash reads this to know when to use it. - `version` (`string | null`): The current version's content hash; null for a skill Bash already has. - `sha256` (`string | null`): A hash of the files as uploaded, which the SDKs and the CLI compute over a folder the same way, to upload only a folder that changed. Null for a skill Bash already has, and for one uploaded before this field. - `files` (`object[]`, may be absent): Its files, on GET /v1/skills/{skill_id}; absent in lists. - `path` (`string`): The file's path inside the skill. - `bytes` (`integer`): Its size, in bytes. - `content` (`string`): Its text. - `created_at` (`string | null`): When it was uploaded; null for a skill Bash already has. - `updated_at` (`string | null`): When its newest version was uploaded; null for a skill Bash already has. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/skills" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/skills/post-v1-skills.md --- # POST /v1/skills > Upload a skill folder as a zip, or as JSON files. In [Skills API](https://vestra.ai/developers/reference/api/skills.md): Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. Upload a skill folder as a zip, or as JSON files. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `files` (`object[]`, required): The skill's files, 1 to 200, with a `SKILL.md` at the top whose frontmatter names it. Limits: 1 to 200 items. - `path` (`string`, required): The file's path inside the skill, such as `SKILL.md` or `scripts/fill.py`, up to 200 characters. Limits: 1 to 200 characters. - `content` (`string`, required): The file's text, up to 1 MB. Limits: at most 1048576 characters. ## Answers 201 - `object` (`"skill"`): Always `skill`. - `id` (`string | null`): The skill's id (`skl_`); null for a skill Bash already has. - `source` (`"company" | "built_in"`): `company`: one you uploaded. `built_in`: one Bash already has. - `slug` (`string`): Its name in a deployment: what `skills` takes for one Bash already has. - `name` (`string`): Its name, from its SKILL.md. - `description` (`string`): What it does, from its SKILL.md: Bash reads this to know when to use it. - `version` (`string | null`): The current version's content hash; null for a skill Bash already has. - `sha256` (`string | null`): A hash of the files as uploaded, which the SDKs and the CLI compute over a folder the same way, to upload only a folder that changed. Null for a skill Bash already has, and for one uploaded before this field. - `files` (`object[]`, may be absent): Its files, on GET /v1/skills/{skill_id}; absent in lists. - `path` (`string`): The file's path inside the skill. - `bytes` (`integer`): Its size, in bytes. - `content` (`string`): Its text. - `created_at` (`string | null`): When it was uploaded; null for a skill Bash already has. - `updated_at` (`string | null`): When its newest version was uploaded; null for a skill Bash already has. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/skills" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "files": [] }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id.md --- # GET /v1/skills/{skill_id} > One skill and its files. In [Skills API](https://vestra.ai/developers/reference/api/skills.md): Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. One skill and its files. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `skill_id` (`string`, required): The skill's id (`skl_...`). ## Answers 200 - `object` (`"skill"`): Always `skill`. - `id` (`string | null`): The skill's id (`skl_`); null for a skill Bash already has. - `source` (`"company" | "built_in"`): `company`: one you uploaded. `built_in`: one Bash already has. - `slug` (`string`): Its name in a deployment: what `skills` takes for one Bash already has. - `name` (`string`): Its name, from its SKILL.md. - `description` (`string`): What it does, from its SKILL.md: Bash reads this to know when to use it. - `version` (`string | null`): The current version's content hash; null for a skill Bash already has. - `sha256` (`string | null`): A hash of the files as uploaded, which the SDKs and the CLI compute over a folder the same way, to upload only a folder that changed. Null for a skill Bash already has, and for one uploaded before this field. - `files` (`object[]`, may be absent): Its files, on GET /v1/skills/{skill_id}; absent in lists. - `path` (`string`): The file's path inside the skill. - `bytes` (`integer`): Its size, in bytes. - `content` (`string`): Its text. - `created_at` (`string | null`): When it was uploaded; null for a skill Bash already has. - `updated_at` (`string | null`): When its newest version was uploaded; null for a skill Bash already has. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/skills/{skill_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/skills/put-v1-skills-skill-id.md --- # PUT /v1/skills/{skill_id} > Upload a new version. In [Skills API](https://vestra.ai/developers/reference/api/skills.md): Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. Upload a new version. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `skill_id` (`string`, required): The skill's id (`skl_...`). Body: - `files` (`object[]`, required): The skill's files, 1 to 200, with a `SKILL.md` at the top whose frontmatter names it. Limits: 1 to 200 items. - `path` (`string`, required): The file's path inside the skill, such as `SKILL.md` or `scripts/fill.py`, up to 200 characters. Limits: 1 to 200 characters. - `content` (`string`, required): The file's text, up to 1 MB. Limits: at most 1048576 characters. ## Answers 200 - `object` (`"skill"`): Always `skill`. - `id` (`string | null`): The skill's id (`skl_`); null for a skill Bash already has. - `source` (`"company" | "built_in"`): `company`: one you uploaded. `built_in`: one Bash already has. - `slug` (`string`): Its name in a deployment: what `skills` takes for one Bash already has. - `name` (`string`): Its name, from its SKILL.md. - `description` (`string`): What it does, from its SKILL.md: Bash reads this to know when to use it. - `version` (`string | null`): The current version's content hash; null for a skill Bash already has. - `sha256` (`string | null`): A hash of the files as uploaded, which the SDKs and the CLI compute over a folder the same way, to upload only a folder that changed. Null for a skill Bash already has, and for one uploaded before this field. - `files` (`object[]`, may be absent): Its files, on GET /v1/skills/{skill_id}; absent in lists. - `path` (`string`): The file's path inside the skill. - `bytes` (`integer`): Its size, in bytes. - `content` (`string`): Its text. - `created_at` (`string | null`): When it was uploaded; null for a skill Bash already has. - `updated_at` (`string | null`): When its newest version was uploaded; null for a skill Bash already has. ## Code **HTTP** ```bash curl -X PUT "$VESTRA_BASE_URL/v1/skills/{skill_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "files": [] }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/skills/delete-v1-skills-skill-id.md --- # DELETE /v1/skills/{skill_id} > Remove it from every user. In [Skills API](https://vestra.ai/developers/reference/api/skills.md): Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. Remove it from every user. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `skill_id` (`string`, required): The skill's id (`skl_...`). ## Answers 200 - `object` (`"skill"`): Always `skill`. - `id` (`string`): The deleted skill's id. - `deleted` (`true`): Always true: it is gone. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/skills/{skill_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/skills/get-v1-skills-skill-id-versions.md --- # GET /v1/skills/{skill_id}/versions > Every version, each with a content hash. In [Skills API](https://vestra.ai/developers/reference/api/skills.md): Skills your Bash may use: upload one, give it new versions, list them. Send `Authorization: Bearer vk_test_...` on every call. Every version, each with a content hash. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `skill_id` (`string`, required): The skill's id (`skl_...`). Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"skill_version"`): Always `skill_version`. - `hash` (`string`): The version's content hash. - `parent_hash` (`string | null`): The version it replaced; null for the first. - `created_at` (`string`): When it was uploaded, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/skills/{skill_id}/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks.md --- # Notebooks API > Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/notebooks](https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks.md): Create a notebook. - `GET` [/v1/notebooks](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks.md): Your notebooks. - `GET` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id.md): One notebook and who may read it. - `PATCH` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/patch-v1-notebooks-notebook-id.md): Rename it, or set who may read it: every user, or named users only. - `DELETE` [/v1/notebooks/{notebook_id}](https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id.md): Delete it. - `POST` [/v1/notebooks/{notebook_id}/documents](https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks-notebook-id-documents.md): Add a document, as a file or as text. - `GET` [/v1/notebooks/{notebook_id}/documents](https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id-documents.md): Its documents. - `DELETE` [/v1/notebooks/{notebook_id}/documents/{document_id}](https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id-documents-document-id.md): Remove one. --- url: https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks.md --- # POST /v1/notebooks > Create a notebook. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Create a notebook. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `name` (`string`, required): Its name, as people read it, 1 to 80 characters. Limits: 1 to 80 characters. Example: `Refund policy`. - `slug` (`string`, required): Its short name: lowercase letters, digits and hyphens, starting with a letter, up to 40. Example: `refund-policy`. - `description` (`string`): What it holds, up to 300 characters: Bash reads this to know when to look in it. Empty by default. Default: empty. Limits: at most 300 characters. - `access` (`object`): Who may read it. Default `{ "mode": "all" }`. Default: `{"mode":"all"}`. One of: `{ mode }`, `{ mode, user_ids }`. ## Answers 201 - `object` (`"notebook"`): Always `notebook`. - `id` (`string`): The notebook's id. Example: `nb_8f2k`. - `slug` (`string`): Its short name. - `name` (`string`): Its name, as people read it. - `description` (`string`): What it holds; empty when none was given. - `access` (`object`): Who may read it. One of: `{ mode }`, `{ mode, user_ids }`. - `document_count` (`integer`): How many documents it holds. - `bytes` (`integer`): The size of its text, in bytes, at most 5 MB. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/notebooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Refund policy", "slug": "refund-policy" }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks.md --- # GET /v1/notebooks > Your notebooks. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Your notebooks. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"notebook"`): Always `notebook`. - `id` (`string`): The notebook's id. Example: `nb_8f2k`. - `slug` (`string`): Its short name. - `name` (`string`): Its name, as people read it. - `description` (`string`): What it holds; empty when none was given. - `access` (`object`): Who may read it. One of: `{ mode }`, `{ mode, user_ids }`. - `document_count` (`integer`): How many documents it holds. - `bytes` (`integer`): The size of its text, in bytes, at most 5 MB. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/notebooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id.md --- # GET /v1/notebooks/{notebook_id} > One notebook and who may read it. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. One notebook and who may read it. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `notebook_id` (`string`, required): The notebook's id (`nb_...`). ## Answers 200 - `object` (`"notebook"`): Always `notebook`. - `id` (`string`): The notebook's id. Example: `nb_8f2k`. - `slug` (`string`): Its short name. - `name` (`string`): Its name, as people read it. - `description` (`string`): What it holds; empty when none was given. - `access` (`object`): Who may read it. One of: `{ mode }`, `{ mode, user_ids }`. - `document_count` (`integer`): How many documents it holds. - `bytes` (`integer`): The size of its text, in bytes, at most 5 MB. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/notebooks/{notebook_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/patch-v1-notebooks-notebook-id.md --- # PATCH /v1/notebooks/{notebook_id} > Rename it, or set who may read it: every user, or named users only. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Rename it, or set who may read it: every user, or named users only. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `notebook_id` (`string`, required): The notebook's id (`nb_...`). Body: - `name` (`string`): A new name, 1 to 80 characters. Limits: 1 to 80 characters. - `description` (`string`): A new description, up to 300 characters. Limits: at most 300 characters. - `access` (`object`): Who may read it, from now on. One of: `{ mode }`, `{ mode, user_ids }`. ## Answers 200 - `object` (`"notebook"`): Always `notebook`. - `id` (`string`): The notebook's id. Example: `nb_8f2k`. - `slug` (`string`): Its short name. - `name` (`string`): Its name, as people read it. - `description` (`string`): What it holds; empty when none was given. - `access` (`object`): Who may read it. One of: `{ mode }`, `{ mode, user_ids }`. - `document_count` (`integer`): How many documents it holds. - `bytes` (`integer`): The size of its text, in bytes, at most 5 MB. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X PATCH "$VESTRA_BASE_URL/v1/notebooks/{notebook_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id.md --- # DELETE /v1/notebooks/{notebook_id} > Delete it. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Delete it. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `notebook_id` (`string`, required): The notebook's id (`nb_...`). ## Answers 200 - `object` (`"notebook"`): Always `notebook`. - `id` (`string`): The notebook's id. Example: `nb_8f2k`. - `deleted` (`true`): Always true: it is gone. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/notebooks/{notebook_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/post-v1-notebooks-notebook-id-documents.md --- # POST /v1/notebooks/{notebook_id}/documents > Add a document, as a file or as text. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Add a document, as a file or as text. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `notebook_id` (`string`, required): The notebook's id (`nb_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `title` (`string`, required): Its title, 1 to 200 characters. Limits: 1 to 200 characters. Example: `Refunds for cancelled flights`. - `text` (`string`, required): Its text, up to 512 KB. Limits: 1 to 524288 characters. ## Answers 201 - `object` (`"document"`): Always `document`. - `id` (`string`): The document's id. - `notebook_id` (`string`): The notebook it is in. - `title` (`string`): Its title. - `content_type` (`string`): The type of its text, such as `text/markdown` or `text/plain`. - `bytes` (`integer`): Its size, in bytes. - `sha256` (`string`): A hash of its text. - `created_at` (`string`): When it was added, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/notebooks/{notebook_id}/documents" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Refunds for cancelled flights", "text": "..." }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/get-v1-notebooks-notebook-id-documents.md --- # GET /v1/notebooks/{notebook_id}/documents > Its documents. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Its documents. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `notebook_id` (`string`, required): The notebook's id (`nb_...`). ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"document"`): Always `document`. - `id` (`string`): The document's id. - `notebook_id` (`string`): The notebook it is in. - `title` (`string`): Its title. - `content_type` (`string`): The type of its text, such as `text/markdown` or `text/plain`. - `bytes` (`integer`): Its size, in bytes. - `sha256` (`string`): A hash of its text. - `created_at` (`string`): When it was added, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/notebooks/{notebook_id}/documents" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/notebooks/delete-v1-notebooks-notebook-id-documents-document-id.md --- # DELETE /v1/notebooks/{notebook_id}/documents/{document_id} > Remove one. In [Notebooks API](https://vestra.ai/developers/reference/api/notebooks.md): Knowledge Bash reads before it answers: notebooks, and the documents in them. Send `Authorization: Bearer vk_test_...` on every call. Remove one. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `notebook_id` (`string`, required): The notebook's id (`nb_...`). - `document_id` (`string`, required): The document's id (`doc_...`). ## Answers 200 - `object` (`"document"`): Always `document`. - `id` (`string`): The removed document's id. - `deleted` (`true`): Always true: it is gone. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/notebooks/{notebook_id}/documents/{document_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/memory.md --- # Memory API > What Bash remembers about each user: read it, search it, add to it, forget it. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/memory](https://vestra.ai/developers/reference/api/memory/get-v1-memory.md): What Bash remembers about one user (user_id), or the company's own Bash: the profile and the memories, newest first. - `POST` [/v1/memory](https://vestra.ai/developers/reference/api/memory/post-v1-memory.md): Tell Bash a fact once, for example 'prefers window seats', for one user (user_id) or the company's own Bash. - `DELETE` [/v1/memory](https://vestra.ai/developers/reference/api/memory/delete-v1-memory.md): Forget everything about one user (user_id), or the company's own Bash. - `GET` [/v1/memory/search](https://vestra.ai/developers/reference/api/memory/get-v1-memory-search.md): Ask the memory a question (q), for one user (user_id) or the company's own Bash. - `DELETE` [/v1/memory/{memory_id}](https://vestra.ai/developers/reference/api/memory/delete-v1-memory-memory-id.md): Forget one thing. --- url: https://vestra.ai/developers/reference/api/memory/get-v1-memory.md --- # GET /v1/memory > What Bash remembers about one user (user_id), or the company's own Bash: the profile and the memories, newest first. In [Memory API](https://vestra.ai/developers/reference/api/memory.md): What Bash remembers about each user: read it, search it, add to it, forget it. Send `Authorization: Bearer vk_test_...` on every call. What Bash remembers about one user (user_id), or the company's own Bash: the profile and the memories, newest first. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`memory`](https://vestra.ai/developers/memory.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): Whose memory, by your id for them. Absent: your company's own Bash's. ## Answers 200 - `profile` (`object | null`): What Bash knows about the user, written up as one profile; null until it has one. - `markdown` (`string`): The profile, as Markdown. - `updated_at` (`string | null`): When it last changed, as an ISO 8601 time; null when unknown. - `data` (`object[]`): The memories, one page of them. - `id` (`string`): The memory (mem_...). - `text` (`string`): What Bash remembers, in plain words. Example: `Prefers a window seat.`. - `kind` (`string`): What kind of fact it is, such as preference or identity. - `source` (`string`): How Bash learned it: user_direct (you or your user said so), tool_observed (seen in an app), upload, screen, or agent_inferred (worked out from the conversation). - `observed_at` (`string`): When Bash learned it, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as cursor for the next page; null on the last. ## Code **TypeScript** ```ts const memory = await vestra.user('traveller-42').memory.list(); ``` **Python** ```python memory = vestra.user("traveller-42").memory.list() ``` **CLI** ```bash vestra memory --user traveller-42 ``` **HTTP** ```bash # Add a fact. Tell Bash something once, and it remembers it. curl "$VESTRA_BASE_URL/v1/memory" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "text": "Prefers a window seat, and never a red-eye." }' # Search what Bash remembers. Ask a question in plain words. curl "$VESTRA_BASE_URL/v1/memory/search?user_id=traveller-42&q=Which%20seat%20does%20she%20like" \ -H "Authorization: Bearer $VESTRA_API_KEY" # List and forget memories. Read the profile and every memory, newest first. Then forget one, or everything. curl "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory/mem_4ka?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user": "traveller-42", "q": "Which seat does she like?" } } } ``` --- url: https://vestra.ai/developers/reference/api/memory/post-v1-memory.md --- # POST /v1/memory > Tell Bash a fact once, for example 'prefers window seats', for one user (user_id) or the company's own Bash. In [Memory API](https://vestra.ai/developers/reference/api/memory.md): What Bash remembers about each user: read it, search it, add to it, forget it. Send `Authorization: Bearer vk_test_...` on every call. Tell Bash a fact once, for example 'prefers window seats', for one user (user_id) or the company's own Bash. Your API key, scope `runtime` or `all`. On the card: [`memory`](https://vestra.ai/developers/memory.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `user_id` (`string`): Whose memory, by your id for them. Absent: your company's own Bash's. Example: `traveller-42`. - `text` (`string`, required): The fact, in plain words: at most 1,000 characters. Limits: 1 to 1000 characters. Example: `Prefers a window seat.`. - `kind` (`"identity" | "relationship" | "commitment" | "preference" | "project_state" | "other"`): What kind of fact it is: who they are (identity), who they know (relationship), what they promised (commitment), what they like (preference), where a piece of work stands (project_state), or other. Default: `other`. ## Answers 201 - `id` (`string`): The memory (mem_...). - `text` (`string`): What Bash remembers, in plain words. Example: `Prefers a window seat.`. - `kind` (`string`): What kind of fact it is, such as preference or identity. - `source` (`string`): How Bash learned it: user_direct (you or your user said so), tool_observed (seen in an app), upload, screen, or agent_inferred (worked out from the conversation). - `observed_at` (`string`): When Bash learned it, as an ISO 8601 time. ## Code **TypeScript** ```ts await vestra.user('traveller-42').memory.add('Prefers a window seat.'); ``` **Python** ```python vestra.user("traveller-42").memory.add("Prefers a window seat.") ``` **CLI** ```bash vestra memory add "Prefers a window seat." --user traveller-42 ``` **HTTP** ```bash # Add a fact. Tell Bash something once, and it remembers it. curl "$VESTRA_BASE_URL/v1/memory" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "text": "Prefers a window seat, and never a red-eye." }' # Search what Bash remembers. Ask a question in plain words. curl "$VESTRA_BASE_URL/v1/memory/search?user_id=traveller-42&q=Which%20seat%20does%20she%20like" \ -H "Authorization: Bearer $VESTRA_API_KEY" # List and forget memories. Read the profile and every memory, newest first. Then forget one, or everything. curl "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory/mem_4ka?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user": "traveller-42", "q": "Which seat does she like?" } } } ``` --- url: https://vestra.ai/developers/reference/api/memory/delete-v1-memory.md --- # DELETE /v1/memory > Forget everything about one user (user_id), or the company's own Bash. In [Memory API](https://vestra.ai/developers/reference/api/memory.md): What Bash remembers about each user: read it, search it, add to it, forget it. Send `Authorization: Bearer vk_test_...` on every call. Forget everything about one user (user_id), or the company's own Bash. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`memory`](https://vestra.ai/developers/memory.md). Every call here is a checked sample. ## Request Query: - `user_id` (`string`): Whose memory, by your id for them. Absent: your company's own Bash's. ## Answers 200 - `user_id` (`string | null`): Whose memory it was, by your id for them; null for your company's own Bash. - `deleted` (`true`): Always true: Bash no longer remembers any of it. - `memories_deleted` (`integer`): How many memories were forgotten. ## Code **TypeScript** ```ts await vestra.user('traveller-42').memory.forgetAll(); ``` **Python** ```python vestra.user("traveller-42").memory.forget_all() ``` **CLI** ```bash vestra memory forget --all --user traveller-42 --yes ``` **HTTP** ```bash # Add a fact. Tell Bash something once, and it remembers it. curl "$VESTRA_BASE_URL/v1/memory" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "text": "Prefers a window seat, and never a red-eye." }' # Search what Bash remembers. Ask a question in plain words. curl "$VESTRA_BASE_URL/v1/memory/search?user_id=traveller-42&q=Which%20seat%20does%20she%20like" \ -H "Authorization: Bearer $VESTRA_API_KEY" # List and forget memories. Read the profile and every memory, newest first. Then forget one, or everything. curl "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory/mem_4ka?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user": "traveller-42", "q": "Which seat does she like?" } } } ``` --- url: https://vestra.ai/developers/reference/api/memory/get-v1-memory-search.md --- # GET /v1/memory/search > Ask the memory a question (q), for one user (user_id) or the company's own Bash. In [Memory API](https://vestra.ai/developers/reference/api/memory.md): What Bash remembers about each user: read it, search it, add to it, forget it. Send `Authorization: Bearer vk_test_...` on every call. Ask the memory a question (q), for one user (user_id) or the company's own Bash. Your API key, scope `runtime` or `all`. On the card: [`memory`](https://vestra.ai/developers/memory.md). Every call here is a checked sample. ## Request Query: - `user_id` (`string`): Whose memory, by your id for them. Absent: your company's own Bash's. - `q` (`string`, required): What to ask the memory, in at most 500 characters. ## Answers 200 - `query` (`string`): What you asked. - `found` (`boolean`): Whether the memory had an answer. - `text` (`string | null`): The answer, in plain words; null when nothing was found. ## Code **TypeScript** ```ts const found = await vestra.user('traveller-42').memory.search('Which seat does she like?'); ``` **Python** ```python found = vestra.user("traveller-42").memory.search("Which seat does she like?") ``` **CLI** ```bash vestra memory search "Which seat does she like?" --user traveller-42 ``` **HTTP** ```bash # Add a fact. Tell Bash something once, and it remembers it. curl "$VESTRA_BASE_URL/v1/memory" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "text": "Prefers a window seat, and never a red-eye." }' # Search what Bash remembers. Ask a question in plain words. curl "$VESTRA_BASE_URL/v1/memory/search?user_id=traveller-42&q=Which%20seat%20does%20she%20like" \ -H "Authorization: Bearer $VESTRA_API_KEY" # List and forget memories. Read the profile and every memory, newest first. Then forget one, or everything. curl "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory/mem_4ka?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user": "traveller-42", "q": "Which seat does she like?" } } } ``` --- url: https://vestra.ai/developers/reference/api/memory/delete-v1-memory-memory-id.md --- # DELETE /v1/memory/{memory_id} > Forget one thing. In [Memory API](https://vestra.ai/developers/reference/api/memory.md): What Bash remembers about each user: read it, search it, add to it, forget it. Send `Authorization: Bearer vk_test_...` on every call. Forget one thing. user_id names whose memory it is. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`memory`](https://vestra.ai/developers/memory.md). Every call here is a checked sample. ## Request Path: - `memory_id` (`string`, required): The memory's id (`mem_...`). Query: - `user_id` (`string`): Whose memory, by your id for them. Absent: your company's own Bash's. ## Answers 200 - `id` (`string`): The memory (mem_...). - `deleted` (`true`): Always true: Bash no longer remembers it. ## Code **TypeScript** ```ts await vestra.user('traveller-42').memory.forget('mem_4ka'); ``` **Python** ```python vestra.user("traveller-42").memory.forget("mem_4ka") ``` **CLI** ```bash vestra memory forget mem_4ka --user traveller-42 ``` **HTTP** ```bash # Add a fact. Tell Bash something once, and it remembers it. curl "$VESTRA_BASE_URL/v1/memory" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "text": "Prefers a window seat, and never a red-eye." }' # Search what Bash remembers. Ask a question in plain words. curl "$VESTRA_BASE_URL/v1/memory/search?user_id=traveller-42&q=Which%20seat%20does%20she%20like" \ -H "Authorization: Bearer $VESTRA_API_KEY" # List and forget memories. Read the profile and every memory, newest first. Then forget one, or everything. curl "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory/mem_4ka?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl -X DELETE "$VESTRA_BASE_URL/v1/memory?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user": "traveller-42", "q": "Which seat does she like?" } } } ``` --- url: https://vestra.ai/developers/reference/api/deployments.md --- # Deployments API > How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/deployments](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments.md): Your deployments. - `POST` [/v1/deployments](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments.md): Create one on base 'bash'. - `GET` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id.md): One deployment, by its id or its name: the live version and the draft. - `PUT` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/put-v1-deployments-deployment-id.md): Apply a whole vestra.yaml, sent as JSON, by the deployment's name: the first call makes it, and a file that changes nothing makes no version (changed: []). - `PATCH` [/v1/deployments/{deployment_id}](https://vestra.ai/developers/reference/api/deployments/patch-v1-deployments-deployment-id.md): Rename, describe or archive it. - `POST` [/v1/deployments/{deployment_id}/versions](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions.md): Save a new version: instructions, language, MCP servers and this version's own tool policy for them (mcp_tool_policy, on top of each server's floor), allowed apps, approval rules, model, memory mode, notebooks, limits per task. - `GET` [/v1/deployments/{deployment_id}/versions](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions.md): Every version. - `GET` [/v1/deployments/{deployment_id}/versions/{version}](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions-version.md): One version, in full. - `GET` [/v1/deployments/{deployment_id}/diff](https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-diff.md): What changed between two versions (?from=&to=). - `POST` [/v1/deployments/{deployment_id}/versions/{version}/promote](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions-version-promote.md): Make it live. - `POST` [/v1/deployments/{deployment_id}/rollback](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-rollback.md): Put an earlier version back live, instantly. - `POST` [/v1/deployments/interview](https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-interview.md): Build a deployment by talking to Bash: describe the job, answer its questions, receive a draft version. --- url: https://vestra.ai/developers/reference/api/deployments/get-v1-deployments.md --- # GET /v1/deployments > Your deployments. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Your deployments. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/deployments" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/post-v1-deployments.md --- # POST /v1/deployments > Create one on base 'bash'. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Create one on base 'bash'. It starts with a draft version. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in every value. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `name` (`string`, required): Its name, as people read it, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Trip rescue`. - `slug` (`string`): Its name in URLs and refs: lowercase letters, digits and dashes, up to 63. Absent: made from `name`. Example: `trip-rescue`. - `description` (`string`): What it is for, up to 2,000 characters. Limits: at most 2000 characters. - `manifest` (`map of any`): The version's settings as a manifest: `instructions`, `language`, `model`, `mcp_servers`, `mcp_tool_policy`, `apps`, `approvals`, `memory`, `notebooks`, `skills`, `tools`, `environment`, `browser` and `limits`. A field left out takes its default. - `note` (`string`): A note on this version, up to 500 characters, such as what changed. Limits: at most 500 characters. Example: `Adds the refund tool.`. ## Answers 201 - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/deployments" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Trip rescue" }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id.md --- # GET /v1/deployments/{deployment_id} > One deployment, by its id or its name: the live version and the draft. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. One deployment, by its id or its name: the live version and the draft. With format=yaml, its latest version as vestra.yaml. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Query: - `format` (`"json" | "yaml"`): `json` (the default): the deployment. `yaml`: its newest version, as vestra.yaml. ## Answers 200 - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash # Publish a change. Send the whole file as JSON, by its name. The first call creates the deployment, and a # file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in # your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with # tool_results. The first deployment you promote becomes the default, and every ask that names no deployment # goes to it. became_default is true when this call made it the default. If the call waits for tests, that # happens when they pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trip-rescue", "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.", "apps": ["gmail", "google-calendar"], "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }], "approvals": ["payment", "booking", "cancellation"], "tests": [{ "name": "Bag allowance", "ask": "How many kilos can I take on booking TN-48213?", "expect": "Says 7 kg of cabin baggage.", "tool_results": { "get_booking": { "cabin_kg": 7 } } }] }' # Read the file and its versions. The file comes back as YAML. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Roll back to the previous version. The version that was live before comes back at once. This needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/put-v1-deployments-deployment-id.md --- # PUT /v1/deployments/{deployment_id} > Apply a whole vestra.yaml, sent as JSON, by the deployment's name: the first call makes it, and a file that changes nothing makes no version (changed: []). In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Apply a whole vestra.yaml, sent as JSON, by the deployment's name: the first call makes it, and a file that changes nothing makes no version (changed: []). MCP servers are registered by URL, and the tool policy the file gives each one is this version's own, on top of the server's floor, from the moment the version runs. Tests become scenarios, and the routines and webhooks the file declares are its own. ?promote=after_tests (the default), now or no promotes in the key's mode. 400 secret_missing names the secrets to store first; 409 mcp_server_shared names the other deployments that run an MCP server whose headers the file would change. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Query: - `promote` (`"after_tests" | "now" | "no"`): When the new version goes live in the key's mode: `after_tests` (the default) once the file's tests pass, `now`, or `no`. Body: - `$schema` (`string`): For editors reading JSON; ignored. Limits: at most 500 characters. - `name` (`string`, required): The deployment's name, as in `PUT /v1/deployments/{name}`: lowercase letters, digits and dashes. Example: `trip-rescue`. - `instructions` (`string`): What Bash does here, and how, in plain words: up to 8,000 characters. Default: empty. Limits: at most 8000 characters. Example: `You help Tripnest travellers fix broken trips. Always offer two options.`. - `language` (`string`): The language Bash speaks: a tag such as `en`, `hi` or `pt-BR`, or `auto` to follow each user. Default: `auto`. Limits: 1 to 35 characters. Example: `hi`. - `model` (`string`): `default`, or a model id from `GET /v1/models`. Default: `default`. Limits: 1 to 100 characters. Example: `default`. - `apps` (`"all" | string[]`): The apps its users may connect: `all`, or a list. Default: `all`. Example: `["gmail","google-calendar"]`. - `tools` (`object[]`): Tools Bash may call: MCP servers by their URL, and functions your own code answers. At most 50. Default: `[]`. Limits: at most 50 items. Example: `[{"function":"get_booking","description":"A booking by its PNR","input":{"pnr":"string"},"read_only":true},{"mcp":"https://mcp.example.com/travel","require_approval":["book_flight"]}]`. - `skills` (`string | (string | object)[]`): Skills Bash may use: a local folder of skills, or a list of folders, company skills by id and skills Bash has by name. At most 100. Default: `[]`. Example: `["./skills/refunds",{"name":"pdf"}]`. - `knowledge` (`string | (string | object)[]`): What Bash reads before it answers: a local folder or file, or a list of them and notebooks by id. At most 50. Default: `[]`. Example: `./policies`. - `approvals` (`("payment" | "booking" | "cancellation" | "send_email" | "publish" | "delete")[]`): What Bash asks the person before doing. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. Default: `["payment","booking","cancellation"]`. Limits: at most 6 items. Example: `["payment","booking","cancellation","send_email"]`. - `memory` (`"vestra" | "customer" | "none"`): What Bash remembers about each user: `vestra` (Bash keeps it), `customer` (your side keeps it) or `none`. Absent: the company's default. Example: `vestra`. - `computer` (`object`): Bash's computer, ready before the first task: a setup script, repositories, environment variables and files. Example: `{"setup":"./setup.sh","env":{"REGION":"ap-south-1"}}`. - `setup` (`string`): A setup script, run once on each computer. Name a local file: the CLI and the SDKs read it and send its text. Limits: at most 20000 characters. Example: `./setup.sh`. - `repos` (`(string | object)[]`): Repositories cloned onto the computer, at most 10. Limits: at most 10 items. Example: `["github.com/acme/booking-tools"]`. - `env` (`map of string`): Environment variables. A credential is `${NAME}`, never a raw value. Example: `{"REGION":"ap-south-1","BOOKING_TOKEN":"${BOOKING_TOKEN}"}`. - `files` (`object[]`): Files uploaded with `POST /v1/files`, placed on the computer. Limits: at most 50 items. Example: `[{"file_id":"file_8f2k","path":"data/fares.csv"}]`. - `file_id` (`string`, required): The file's `file_` id. - `path` (`string`, required): Where it goes on the computer. Limits: 1 to 200 characters. - `browser` (`object`): The sites a browser fetch may open, and the pages the browser never opens or acts on. Absent: a fetch opens nothing, and no page is closed. Example: `{"allowed_domains":["ixigo.com"],"blocked_paths":["ixigo.com/payments/"]}`. - `allowed_domains` (`string[]`): The sites it may open, as bare host names: `ixigo.com` also allows `www.ixigo.com`. Up to 20. Limits: 1 to 20 items. - `blocked_paths` (`string[]`): Pages the browser never opens or acts on, as a site and the start of a path, such as `ixigo.com/payments/`. Up to 20. Limits: 1 to 20 items. - `routines` (`object[]`): Jobs Bash runs on a schedule or on a call, for your company or for each user. At most 50. Default: `[]`. Limits: at most 50 items. Example: `[{"ask":"Check today's trips for delays","every":"weekday at 08:00","for":"each customer"}]`. - `name` (`string`): Its name, which finds the routine again on the next apply. Absent: the start of `ask`. Limits: 1 to 200 characters. - `ask` (`string`, required): What Bash does on each run, in plain words. Limits: 1 to 8000 characters. - `every` (`string`): When it runs, in plain words: `weekday at 08:00`, `mon, wed, fri at 18:30`, `30 minutes`, `month on the 1st at 09:00`. Give one of `every`, `schedule` or `on`. Limits: 1 to 100 characters. - `schedule` (`object`): When it runs, as cron. - `cron` (`string`, required): A cron expression: `0 7 * * *` runs at 7:00 every day. Limits: 1 to 120 characters. - `timezone` (`string`): The zone the cron is read in, an IANA name such as `Asia/Kolkata`. Limits: 1 to 64 characters. - `on` (`"call"`): `call`: it runs whenever your system posts to its `trigger_url`. - `filters` (`map of any`): With `on: call`, what a call's payload must match for the routine to run. - `timezone` (`string`): The zone `every` is read in, an IANA name. Absent: each user's own (or your company's own Bash's), then UTC. Limits: 1 to 64 characters. - `for` (`"us" | "each customer"`, required): `us`: your company's own Bash. `each customer`: a copy for every user, made the first time they talk to Bash here. - `enabled` (`boolean`): false keeps the routine but stops its runs. A routine for each user is on while it is in the file. - `limits` (`object`): What one task may spend. Default: `{"per_task_usd":2}`. Example: `{"per_task_usd":5}`. - `per_task_usd` (`number`): The most one task may spend, in dollars: above 0, at most 100. Absent: 2. Default: `2`. Limits: above 0, at most 100. - `tests` (`object[]`): Tests each new version runs before it is promoted. At most 20. Default: `[]`. Limits: at most 20 items. Example: `[{"name":"Bag allowance","ask":"How many kilos can I take on booking TN-48213?","expect":"Says 7 kg of cabin baggage.","tool_results":{"get_booking":{"cabin_kg":7}}}]`. - `name` (`string`): Its name, which finds the test again on the next apply. Absent: the start of `ask`. Limits: 1 to 120 characters. - `ask` (`string`, required): What the test user wants, in their words. Limits: 1 to 2000 characters. - `expect` (`string | string[]`, required): What a pass looks like: one line, or up to 12. - `never` (`string | string[]`): What must never happen: one line, or up to 12. - `persona` (`string`): Who the test user is. Absent: a person who uses the company's app. Limits: 1 to 2000 characters. - `tool_results` (`map of any`): What each tool answers in this test: a function tool by its name, and a tool of an MCP server as . (the server's slug, from its URL's host), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. - `approves` (`boolean`): true: the test user says yes to each approval for a tool of your MCP server, which plays its `tool_results` entry or does not run, so the test can check what Bash does after a yes. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. - `webhooks` (`map of (string | "*")[]`): Where events go: each https URL, with the events it gets, or `*` for every one. Default: `{}`. Example: `{"https://example.com/vestra":["task.completed","interaction.created"]}`. ## Answers 200 or 201 - `object` (`"deployment_apply"`): Always `deployment_apply`. - `deployment` (`object`): The deployment, after the apply. - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `version` (`integer`): The version this file is: new, or the latest when nothing changed. - `changed` (`string[]`): The file's fields that differ from the version before, in the file's order; empty when nothing changed. Example: `["instructions","tools"]`. - `tests` (`object | null`): The test run this apply started; null when it started none. - `id` (`string`): The test run's id. Example: `trun_8f2k`. - `object` (`"test_run"`): Always `test_run`. - `deployment_id` (`string`): The deployment it tests. - `version` (`integer`): The version it tests. - `status` (`"queued" | "running" | "passed" | "failed" | "error"`): `queued` or `running` while it plays. `passed`: every scenario passed. `failed`: one or more did not. `error`: it could not finish, as `error_code` says. - `error_code` (`string | null`): Why it ended in `error`: `interrupted` (a restart stopped it too often), `test_wallet_empty`, `judge_unreadable` (the grader's answer could not be read) or `internal`. Null otherwise. - `scenario_count` (`integer`): How many scenarios it plays. - `passed_count` (`integer`): How many have passed so far. - `resumes` (`integer`): How many times a restart interrupted it and it played on from where it was, at most 2. - `promoted` (`object | null`): What its pass promoted, for an apply that promotes after its tests; null when it promoted nothing (none asked, refused, or it did not pass). - `mode` (`"live" | "test"`): The mode the version went live in. - `version` (`integer`): The version that went live. - `became_default` (`boolean`): True when that promotion made the deployment the company's default: an ask with no deployment now reaches it. - `cost_usd` (`number`): What the run cost, in billed US dollars. - `created_at` (`string`): When it was asked for, as an ISO 8601 date and time in UTC. - `started_at` (`string | null`): When it began to play; null while queued. - `finished_at` (`string | null`): When it ended; null while it plays. - `results` (`object[]`, may be absent): Each scenario's result, on GET /v1/test-runs/{test_run_id}; absent in lists. - `object` (`"test_result"`): Always `test_result`. - `scenario_id` (`string | null`): The scenario it played; null when the scenario was deleted since. - `scenario` (`object`): The scenario as it was played. - `verdict` (`"passed" | "failed" | "error"`): `passed`: every line of success met and nothing it must never do. `failed`: not so. `error`: it could not be played to the end. - `reason` (`string`): Why, in a sentence or two. - `criteria` (`object[]`): Each line of success, graded. - `must_never` (`object[]`): Each thing it must never do, graded. - `approvals_asked` (`object[]`): What Bash asked the test user's yes for, and what it said. - `transcript` (`object[]`): The conversation, line by line. - `conversation_id` (`string | null`): The test conversation; null when it never started. - `cost_usd` (`number`): What playing it cost, in billed US dollars. - `promote` (`object`): Whether, and when, the version goes live. - `when` (`"after_tests" | "now" | "no"`): When it was asked to go live: `after_tests`, `now` or `no`. - `mode` (`"live" | "test"`): The key's mode: `live` moves `live_version`, `test` moves `draft_version`. - `status` (`"promoted" | "already" | "waiting" | "not_promoted"`): `promoted`: this call moved it. `already`: that mode already runs this version. `waiting`: its tests run first. `not_promoted`: see `code`. - `code` (`string | null`): Why it was not promoted, as an error code; null when you sent `promote=no`. After `promoted` or `already`: the first thing in the file that could not be made in this mode, such as a missing secret. Fix it and apply the file again. - `message` (`string | null`): The same, in a sentence a person can read; null with no `code`. - `new_webhooks` (`object[]`): The webhook endpoints this call made, each with its signing secret. - `id` (`string`): The webhook endpoint's id. - `url` (`string`): Where its events go. - `secret` (`string`): Its signing secret, shown here once: store it to check each delivery. - `became_default` (`boolean`): True when this made the deployment the company's default, which it had none of: an ask that names no deployment now reaches it. A promotion that waits for its tests sets it when they pass. ## Code **TypeScript** ```ts const applied = await vestra.deploy('vestra.yaml'); ``` **Python** ```python applied = vestra.deploy("vestra.yaml") ``` **CLI** ```bash vestra deploy ``` **HTTP** ```bash # Publish a change. Send the whole file as JSON, by its name. The first call creates the deployment, and a # file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in # your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with # tool_results. The first deployment you promote becomes the default, and every ask that names no deployment # goes to it. became_default is true when this call made it the default. If the call waits for tests, that # happens when they pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trip-rescue", "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.", "apps": ["gmail", "google-calendar"], "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }], "approvals": ["payment", "booking", "cancellation"], "tests": [{ "name": "Bag allowance", "ask": "How many kilos can I take on booking TN-48213?", "expect": "Says 7 kg of cabin baggage.", "tool_results": { "get_booking": { "cabin_kg": 7 } } }] }' # Read the file and its versions. The file comes back as YAML. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Roll back to the previous version. The version that was live before comes back at once. This needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/patch-v1-deployments-deployment-id.md --- # PATCH /v1/deployments/{deployment_id} > Rename, describe or archive it. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Rename, describe or archive it. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Body: - `name` (`string`): A new name, 1 to 120 characters. Limits: 1 to 120 characters. - `description` (`string`): A new description, up to 2,000 characters. Limits: at most 2000 characters. - `status` (`"active" | "archived"`): `archived` puts it away: no new conversation or version reaches it. `active` brings it back. ## Answers 200 - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X PATCH "$VESTRA_BASE_URL/v1/deployments/{deployment_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions.md --- # POST /v1/deployments/{deployment_id}/versions > Save a new version: instructions, language, MCP servers and this version's own tool policy for them (mcp_tool_policy, on top of each server's floor), allowed apps, approval rules, model, memory mode, notebooks, limits per task. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Save a new version: instructions, language, MCP servers and this version's own tool policy for them (mcp_tool_policy, on top of each server's floor), allowed apps, approval rules, model, memory mode, notebooks, limits per task. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `manifest` (`map of any`, required): The version's settings as a manifest: `instructions`, `language`, `model`, `mcp_servers`, `mcp_tool_policy`, `apps`, `approvals`, `memory`, `notebooks`, `skills`, `tools`, `environment`, `browser` and `limits`. A field left out takes its default. - `note` (`string`): A note on this version, up to 500 characters, such as what changed. Limits: at most 500 characters. Example: `Adds the refund tool.`. ## Answers 201 - `object` (`"deployment_version"`): Always `deployment_version`. - `deployment_id` (`string`): The deployment's id. Example: `dep_8f2k`. - `version` (`integer`): A version number: 1 is the first, and each save adds one. - `manifest` (`object`): The version's settings, every default filled in. - `base` (`"bash"`): What the deployment is built on: always `bash`. Default: `bash`. - `instructions` (`string`): What Bash does here, and how, in plain words, up to 8,000 characters. Empty by default. Default: empty. Limits: at most 8000 characters. - `language` (`string`): The language Bash speaks: a tag such as `en`, `hi` or `pt-BR`, or `auto` (the default) to follow each user. Default: `auto`. - `mcp_servers` (`string[]`): The MCP servers Bash may call, by their `mcp_` ids (POST /v1/mcp-servers). Default: `[]`. - `mcp_tool_policy` (`object[]`, may be absent): This version's own tool policy for its MCP servers, on top of each server's floor. Absent (every version saved before it): each server's own policy alone. Limits: at most 50 items. - `server` (`string`): The MCP server, by its `mcp_` id, one `mcp_servers` lists. - `deny` (`string[]`): Tools of the server this version never calls, on top of the server's own floor. Default: `[]`. Limits: at most 200 items. - `require_approval` (`string[]`): Tools of the server that wait for the person's yes in this version, on top of the server's own floor. Default: `[]`. Limits: at most 200 items. - `run_without_asking` (`string[]`, may be absent): Tools of the server this version runs without waiting for the person's yes. Every other tool of the server waits for it, unless the server marks the tool read-only (readOnlyHint). Limits: at most 200 items. - `apps` (`object`): Which apps its users may connect. Default: `{"allow":"all"}`. - `allow` (`"all" | string[]`): The apps its users may connect: `all` (the default), or a list such as `["gmail"]`. Default: `all`. - `approvals` (`object`): What Bash asks the person before doing. Default: `{"always":["payment","booking","cancellation"]}`. - `always` (`("payment" | "booking" | "cancellation" | "send_email" | "publish" | "delete")[]`): What Bash asks the person before doing: any of `payment`, `booking`, `cancellation`, `send_email`, `publish` and `delete`. Default the first three. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. Default: `["payment","booking","cancellation"]`. - `model` (`string`): `default` (the default), or a model id from GET /v1/models. Default: `default`. - `memory` (`object`): What Bash remembers about each user here. Default: `{}`. - `mode` (`"vestra" | "customer" | "none"`, may be absent): `vestra`, `customer` or `none`. Absent: the company's `memory_mode`. - `notebooks` (`string[]`): What Bash reads before it answers: notebooks, by their `nb_` ids. Default: `[]`. - `skills` (`string[]`): Skills Bash may use: company skills by their `skl_` ids, or skills Bash already has, by name. Default: `[]`. - `tools` (`object[]`): Tools your own code answers: Bash's call reaches you as an interaction of type `tool_call`, after the person's yes unless the tool is marked `read_only` or `run_without_asking`. Default: `[]`. - `name` (`string`): The tool's name: lowercase letters, digits and underscores, starting with a letter, up to 64. Example: `get_booking`. - `description` (`string`): What the tool does, so Bash knows when to call it, up to 1024 characters. Limits: 1 to 1024 characters. Example: `A booking by its PNR`. - `input_schema` (`map of any`): Its arguments, as JSON Schema with `type: object`. Bash's call fills them in. Example: `{"type":"object","properties":{"pnr":{"type":"string"}},"required":["pnr"]}`. - `timeout_s` (`integer`): Seconds Bash waits for your answer, 5 to 600. Default 120; after it, the call expires. Default: `120`. Limits: 5 to 600. - `read_only` (`boolean`, may be absent): true: the tool changes nothing (a lookup), so Bash calls it without waiting for the person's yes. Absent: false. - `run_without_asking` (`boolean`, may be absent): true: the tool changes something that needs no yes, so Bash calls it without waiting for the person's yes. Absent: false. - `environment` (`object`, may be absent): Bash's computer, ready before the first task: a setup script, environment variables, repositories and files. Absent: a plain computer. - `setup_script` (`string`, may be absent): A shell script run once on each computer before its first task, and again when the environment changes, up to 20,000 characters. Absent: none. Limits: at most 20000 characters. - `env` (`map of string`): Environment variables on the computer, by name. A credential is `${secret:NAME}`, filled in from your secrets, never a raw value. Default: `{}`. Example: `{"REGION":"ap-south-1"}`. - `repos` (`object[]`): Repositories cloned onto the computer. Default: `[]`. - `files` (`object[]`): Files placed on the computer. Default: `[]`. - `browser` (`object`, may be absent): The sites a browser fetch may open, and the pages the browser never opens or acts on. Absent: a fetch opens nothing, and no page is closed. - `allowed_domains` (`string[]`, may be absent): The sites a browser fetch may open, as bare host names such as `ixigo.com`, which also allows `www.ixigo.com`. Up to 20. Absent: a browser fetch opens nothing. Limits: 1 to 20 items. Example: `["ixigo.com"]`. - `blocked_paths` (`string[]`, may be absent): Pages the browser never opens or acts on, as a site and the start of a path, such as `ixigo.com/payments/`. Enforced in code for a browser fetch (403 page_blocked) and Bash's own browser tools. Up to 20. Limits: 1 to 20 items. Example: `["ixigo.com/payments/"]`. - `routines` (`object[]`): Not used: a version holds no routines. Write them in vestra.yaml, or make them with POST /v1/routines. Default: `[]`. - `name` (`string`): The routine's name. - `schedule` (`string`): When it runs. - `message` (`string`): What it asks. - `limits` (`object`): What one piece of work may spend. There is no time limit. Default: `{"spend_per_task_usd":2}`. - `spend_per_task_usd` (`number`): The most one piece of work may spend, in US dollars, above 0 and at most 100. Default 2. Default: `2`. Limits: above 0, at most 100. - `manifest_sha256` (`string`): A hash of the settings: two versions with the same hash run the same. - `skill_pins` (`object[]`): The company skills this version runs, each at the version of the skill current when it was saved. - `skill` (`string`): The company skill, by its `skl_` id. - `slug` (`string`): The skill's name. - `pressing_sha256` (`string | null`): The content hash of the skill's version when this version was saved: the one it runs. Null when none was recorded. - `note` (`string | null`): The note it was saved with; null when none. - `created_by` (`string`): Who saved it: a key's id (`key_...`), `console` (a person in the console), `client` (a session token from your app) or `interview` (the draft the interview wrote). - `created_at` (`string`): When it was saved, as an ISO 8601 date and time in UTC. - `is_draft` (`boolean`): True when test keys run this version. - `is_live` (`boolean`): True when live keys run it. - `test_verdict` (`"passed" | "failed" | "none"`, may be absent): Its latest test run's verdict: `passed`, `failed`, or `none` (never tested). Only when you read one version (`GET .../versions/{version}`); absent in the list. - `interview` (`object`, may be absent): The interview's closing words, on the version the interview saved; absent on every other version. - `summary` (`string`): What the draft sets Bash up to do, in the interview's words. - `tools` (`object[]`): The tools the file declares that your own code answers. - `name` (`string`): The tool's name. - `description` (`string`): What it does. - `per_task_usd` (`number`): The spending cap per piece of work, in US dollars. - `per_task_usd_is_default` (`boolean`): True when that cap is the default, 2, because the file left it. - `tests` (`integer`): How many starter tests the file holds. - `next_steps` (`string[]`): What to do next, one plain line each, in order. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "manifest": {} }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions.md --- # GET /v1/deployments/{deployment_id}/versions > Every version. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Every version. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"deployment_version"`): Always `deployment_version`. - `deployment_id` (`string`): The deployment's id. Example: `dep_8f2k`. - `version` (`integer`): A version number: 1 is the first, and each save adds one. - `manifest` (`object`): The version's settings, every default filled in. - `base` (`"bash"`): What the deployment is built on: always `bash`. Default: `bash`. - `instructions` (`string`): What Bash does here, and how, in plain words, up to 8,000 characters. Empty by default. Default: empty. Limits: at most 8000 characters. - `language` (`string`): The language Bash speaks: a tag such as `en`, `hi` or `pt-BR`, or `auto` (the default) to follow each user. Default: `auto`. - `mcp_servers` (`string[]`): The MCP servers Bash may call, by their `mcp_` ids (POST /v1/mcp-servers). Default: `[]`. - `mcp_tool_policy` (`object[]`, may be absent): This version's own tool policy for its MCP servers, on top of each server's floor. Absent (every version saved before it): each server's own policy alone. Limits: at most 50 items. - `apps` (`object`): Which apps its users may connect. Default: `{"allow":"all"}`. - `approvals` (`object`): What Bash asks the person before doing. Default: `{"always":["payment","booking","cancellation"]}`. - `model` (`string`): `default` (the default), or a model id from GET /v1/models. Default: `default`. - `memory` (`object`): What Bash remembers about each user here. Default: `{}`. - `notebooks` (`string[]`): What Bash reads before it answers: notebooks, by their `nb_` ids. Default: `[]`. - `skills` (`string[]`): Skills Bash may use: company skills by their `skl_` ids, or skills Bash already has, by name. Default: `[]`. - `tools` (`object[]`): Tools your own code answers: Bash's call reaches you as an interaction of type `tool_call`, after the person's yes unless the tool is marked `read_only` or `run_without_asking`. Default: `[]`. - `environment` (`object`, may be absent): Bash's computer, ready before the first task: a setup script, environment variables, repositories and files. Absent: a plain computer. - `browser` (`object`, may be absent): The sites a browser fetch may open, and the pages the browser never opens or acts on. Absent: a fetch opens nothing, and no page is closed. - `routines` (`object[]`): Not used: a version holds no routines. Write them in vestra.yaml, or make them with POST /v1/routines. Default: `[]`. - `limits` (`object`): What one piece of work may spend. There is no time limit. Default: `{"spend_per_task_usd":2}`. - `manifest_sha256` (`string`): A hash of the settings: two versions with the same hash run the same. - `skill_pins` (`object[]`): The company skills this version runs, each at the version of the skill current when it was saved. - `skill` (`string`): The company skill, by its `skl_` id. - `slug` (`string`): The skill's name. - `pressing_sha256` (`string | null`): The content hash of the skill's version when this version was saved: the one it runs. Null when none was recorded. - `note` (`string | null`): The note it was saved with; null when none. - `created_by` (`string`): Who saved it: a key's id (`key_...`), `console` (a person in the console), `client` (a session token from your app) or `interview` (the draft the interview wrote). - `created_at` (`string`): When it was saved, as an ISO 8601 date and time in UTC. - `is_draft` (`boolean`): True when test keys run this version. - `is_live` (`boolean`): True when live keys run it. - `test_verdict` (`"passed" | "failed" | "none"`, may be absent): Its latest test run's verdict: `passed`, `failed`, or `none` (never tested). Only when you read one version (`GET .../versions/{version}`); absent in the list. - `interview` (`object`, may be absent): The interview's closing words, on the version the interview saved; absent on every other version. - `summary` (`string`): What the draft sets Bash up to do, in the interview's words. - `tools` (`object[]`): The tools the file declares that your own code answers. - `per_task_usd` (`number`): The spending cap per piece of work, in US dollars. - `per_task_usd_is_default` (`boolean`): True when that cap is the default, 2, because the file left it. - `tests` (`integer`): How many starter tests the file holds. - `next_steps` (`string[]`): What to do next, one plain line each, in order. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash # Publish a change. Send the whole file as JSON, by its name. The first call creates the deployment, and a # file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in # your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with # tool_results. The first deployment you promote becomes the default, and every ask that names no deployment # goes to it. became_default is true when this call made it the default. If the call waits for tests, that # happens when they pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trip-rescue", "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.", "apps": ["gmail", "google-calendar"], "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }], "approvals": ["payment", "booking", "cancellation"], "tests": [{ "name": "Bag allowance", "ask": "How many kilos can I take on booking TN-48213?", "expect": "Says 7 kg of cabin baggage.", "tool_results": { "get_booking": { "cabin_kg": 7 } } }] }' # Read the file and its versions. The file comes back as YAML. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Roll back to the previous version. The version that was live before comes back at once. This needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-versions-version.md --- # GET /v1/deployments/{deployment_id}/versions/{version} > One version, in full. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. One version, in full. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. - `version` (`string`, required): The version number: 1 is the first, and each save adds one. ## Answers 200 - `object` (`"deployment_version"`): Always `deployment_version`. - `deployment_id` (`string`): The deployment's id. Example: `dep_8f2k`. - `version` (`integer`): A version number: 1 is the first, and each save adds one. - `manifest` (`object`): The version's settings, every default filled in. - `base` (`"bash"`): What the deployment is built on: always `bash`. Default: `bash`. - `instructions` (`string`): What Bash does here, and how, in plain words, up to 8,000 characters. Empty by default. Default: empty. Limits: at most 8000 characters. - `language` (`string`): The language Bash speaks: a tag such as `en`, `hi` or `pt-BR`, or `auto` (the default) to follow each user. Default: `auto`. - `mcp_servers` (`string[]`): The MCP servers Bash may call, by their `mcp_` ids (POST /v1/mcp-servers). Default: `[]`. - `mcp_tool_policy` (`object[]`, may be absent): This version's own tool policy for its MCP servers, on top of each server's floor. Absent (every version saved before it): each server's own policy alone. Limits: at most 50 items. - `server` (`string`): The MCP server, by its `mcp_` id, one `mcp_servers` lists. - `deny` (`string[]`): Tools of the server this version never calls, on top of the server's own floor. Default: `[]`. Limits: at most 200 items. - `require_approval` (`string[]`): Tools of the server that wait for the person's yes in this version, on top of the server's own floor. Default: `[]`. Limits: at most 200 items. - `run_without_asking` (`string[]`, may be absent): Tools of the server this version runs without waiting for the person's yes. Every other tool of the server waits for it, unless the server marks the tool read-only (readOnlyHint). Limits: at most 200 items. - `apps` (`object`): Which apps its users may connect. Default: `{"allow":"all"}`. - `allow` (`"all" | string[]`): The apps its users may connect: `all` (the default), or a list such as `["gmail"]`. Default: `all`. - `approvals` (`object`): What Bash asks the person before doing. Default: `{"always":["payment","booking","cancellation"]}`. - `always` (`("payment" | "booking" | "cancellation" | "send_email" | "publish" | "delete")[]`): What Bash asks the person before doing: any of `payment`, `booking`, `cancellation`, `send_email`, `publish` and `delete`. Default the first three. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. Default: `["payment","booking","cancellation"]`. - `model` (`string`): `default` (the default), or a model id from GET /v1/models. Default: `default`. - `memory` (`object`): What Bash remembers about each user here. Default: `{}`. - `mode` (`"vestra" | "customer" | "none"`, may be absent): `vestra`, `customer` or `none`. Absent: the company's `memory_mode`. - `notebooks` (`string[]`): What Bash reads before it answers: notebooks, by their `nb_` ids. Default: `[]`. - `skills` (`string[]`): Skills Bash may use: company skills by their `skl_` ids, or skills Bash already has, by name. Default: `[]`. - `tools` (`object[]`): Tools your own code answers: Bash's call reaches you as an interaction of type `tool_call`, after the person's yes unless the tool is marked `read_only` or `run_without_asking`. Default: `[]`. - `name` (`string`): The tool's name: lowercase letters, digits and underscores, starting with a letter, up to 64. Example: `get_booking`. - `description` (`string`): What the tool does, so Bash knows when to call it, up to 1024 characters. Limits: 1 to 1024 characters. Example: `A booking by its PNR`. - `input_schema` (`map of any`): Its arguments, as JSON Schema with `type: object`. Bash's call fills them in. Example: `{"type":"object","properties":{"pnr":{"type":"string"}},"required":["pnr"]}`. - `timeout_s` (`integer`): Seconds Bash waits for your answer, 5 to 600. Default 120; after it, the call expires. Default: `120`. Limits: 5 to 600. - `read_only` (`boolean`, may be absent): true: the tool changes nothing (a lookup), so Bash calls it without waiting for the person's yes. Absent: false. - `run_without_asking` (`boolean`, may be absent): true: the tool changes something that needs no yes, so Bash calls it without waiting for the person's yes. Absent: false. - `environment` (`object`, may be absent): Bash's computer, ready before the first task: a setup script, environment variables, repositories and files. Absent: a plain computer. - `setup_script` (`string`, may be absent): A shell script run once on each computer before its first task, and again when the environment changes, up to 20,000 characters. Absent: none. Limits: at most 20000 characters. - `env` (`map of string`): Environment variables on the computer, by name. A credential is `${secret:NAME}`, filled in from your secrets, never a raw value. Default: `{}`. Example: `{"REGION":"ap-south-1"}`. - `repos` (`object[]`): Repositories cloned onto the computer. Default: `[]`. - `files` (`object[]`): Files placed on the computer. Default: `[]`. - `browser` (`object`, may be absent): The sites a browser fetch may open, and the pages the browser never opens or acts on. Absent: a fetch opens nothing, and no page is closed. - `allowed_domains` (`string[]`, may be absent): The sites a browser fetch may open, as bare host names such as `ixigo.com`, which also allows `www.ixigo.com`. Up to 20. Absent: a browser fetch opens nothing. Limits: 1 to 20 items. Example: `["ixigo.com"]`. - `blocked_paths` (`string[]`, may be absent): Pages the browser never opens or acts on, as a site and the start of a path, such as `ixigo.com/payments/`. Enforced in code for a browser fetch (403 page_blocked) and Bash's own browser tools. Up to 20. Limits: 1 to 20 items. Example: `["ixigo.com/payments/"]`. - `routines` (`object[]`): Not used: a version holds no routines. Write them in vestra.yaml, or make them with POST /v1/routines. Default: `[]`. - `name` (`string`): The routine's name. - `schedule` (`string`): When it runs. - `message` (`string`): What it asks. - `limits` (`object`): What one piece of work may spend. There is no time limit. Default: `{"spend_per_task_usd":2}`. - `spend_per_task_usd` (`number`): The most one piece of work may spend, in US dollars, above 0 and at most 100. Default 2. Default: `2`. Limits: above 0, at most 100. - `manifest_sha256` (`string`): A hash of the settings: two versions with the same hash run the same. - `skill_pins` (`object[]`): The company skills this version runs, each at the version of the skill current when it was saved. - `skill` (`string`): The company skill, by its `skl_` id. - `slug` (`string`): The skill's name. - `pressing_sha256` (`string | null`): The content hash of the skill's version when this version was saved: the one it runs. Null when none was recorded. - `note` (`string | null`): The note it was saved with; null when none. - `created_by` (`string`): Who saved it: a key's id (`key_...`), `console` (a person in the console), `client` (a session token from your app) or `interview` (the draft the interview wrote). - `created_at` (`string`): When it was saved, as an ISO 8601 date and time in UTC. - `is_draft` (`boolean`): True when test keys run this version. - `is_live` (`boolean`): True when live keys run it. - `test_verdict` (`"passed" | "failed" | "none"`, may be absent): Its latest test run's verdict: `passed`, `failed`, or `none` (never tested). Only when you read one version (`GET .../versions/{version}`); absent in the list. - `interview` (`object`, may be absent): The interview's closing words, on the version the interview saved; absent on every other version. - `summary` (`string`): What the draft sets Bash up to do, in the interview's words. - `tools` (`object[]`): The tools the file declares that your own code answers. - `name` (`string`): The tool's name. - `description` (`string`): What it does. - `per_task_usd` (`number`): The spending cap per piece of work, in US dollars. - `per_task_usd_is_default` (`boolean`): True when that cap is the default, 2, because the file left it. - `tests` (`integer`): How many starter tests the file holds. - `next_steps` (`string[]`): What to do next, one plain line each, in order. ## Code **HTTP** ```bash # Describe the job. The interview runs as a conversation, so its questions arrive like any other reply. curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Trip rescue", "slug": "trip-rescue", "job": "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking." }' # Answer its questions. Follow the conversation the response names, and reply in it. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_iv7/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/conversations/conv_iv7/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Ask first before paying a fare difference." }' # Save the draft to your repo. When the interview ends, download the draft as vestra.yaml, starter tests # included. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o vestra.yaml # Read the closing words. The draft version (the deployment's draft_version) has them in its interview field: # what it set up, the tools your code must answer, the spending cap per task, the tests, and the next steps. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue/versions/2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/get-v1-deployments-deployment-id-diff.md --- # GET /v1/deployments/{deployment_id}/diff > What changed between two versions (?from=&to=). In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. What changed between two versions (?from=&to=). Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Query: - `from` (`integer`, required): The version to compare from. - `to` (`integer`, required): The version to compare to. ## Answers 200 - `object` (`"deployment_diff"`): Always `deployment_diff`. - `deployment_id` (`string`): The deployment's id. Example: `dep_8f2k`. - `from` (`integer`): The version compared from. - `to` (`integer`): The version compared to. - `changes` (`object[]`): Each setting that differs; empty when the two run the same. - `path` (`string`): The setting that changed, as a path such as `limits.spend_per_task_usd`. - `from` (`any`): Its value in the version compared from; absent when it was not set. - `to` (`any`): Its value in the version compared to; absent when it is not set. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/diff?from=...&to=..." \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-versions-version-promote.md --- # POST /v1/deployments/{deployment_id}/versions/{version}/promote > Make it live. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Make it live. Refused unless its latest test run passed. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. - `version` (`string`, required): The version number: 1 is the first, and each save adds one. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 200 - `deployment` (`object`): The deployment, after the promotion. - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `promotion` (`object | null`): What moved; null when the version was already live, so nothing did. - `object` (`"deployment_promotion"`): Always `deployment_promotion`. - `deployment_id` (`string`): The deployment's id. Example: `dep_8f2k`. - `action` (`"promote" | "rollback"`): `promote` (a version went live) or `rollback` (an earlier one went back live). - `from_version` (`integer | null`): The version live before; null when none was. - `to_version` (`integer`): The version live now. - `verdict` (`"passed" | "failed" | "none" | "rollback"`): The version's tests when it went live: `passed`, `none` (there were none to run), or `rollback`. - `created_at` (`string`): When it went live, as an ISO 8601 date and time in UTC. - `became_default` (`boolean`): True when this made the deployment the company's default: an ask that names no deployment now reaches it. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/versions/{version}/promote" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-deployment-id-rollback.md --- # POST /v1/deployments/{deployment_id}/rollback > Put an earlier version back live, instantly. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Put an earlier version back live, instantly. Your API key, scope `setup` or `all`. On the card: [`deploy`](https://vestra.ai/developers/deploy.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, if you send one: - `version` (`integer`): The earlier version to put back live. Absent: the one live before this. Limits: at least 1. ## Answers 200 - `deployment` (`object`): The deployment, after the promotion. - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `promotion` (`object | null`): What moved; null when the version was already live, so nothing did. - `object` (`"deployment_promotion"`): Always `deployment_promotion`. - `deployment_id` (`string`): The deployment's id. Example: `dep_8f2k`. - `action` (`"promote" | "rollback"`): `promote` (a version went live) or `rollback` (an earlier one went back live). - `from_version` (`integer | null`): The version live before; null when none was. - `to_version` (`integer`): The version live now. - `verdict` (`"passed" | "failed" | "none" | "rollback"`): The version's tests when it went live: `passed`, `none` (there were none to run), or `rollback`. - `created_at` (`string`): When it went live, as an ISO 8601 date and time in UTC. - `became_default` (`boolean`): True when this made the deployment the company's default: an ask that names no deployment now reaches it. ## Code **TypeScript** ```ts const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); await live.deploy.rollback('travel-help'); ``` **Python** ```python live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) live.deploy.rollback("travel-help") ``` **CLI** ```bash VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --rollback travel-help ``` **HTTP** ```bash # Publish a change. Send the whole file as JSON, by its name. The first call creates the deployment, and a # file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in # your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with # tool_results. The first deployment you promote becomes the default, and every ask that names no deployment # goes to it. became_default is true when this call made it the default. If the call waits for tests, that # happens when they pass. curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "trip-rescue", "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.", "apps": ["gmail", "google-calendar"], "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }], "approvals": ["payment", "booking", "cancellation"], "tests": [{ "name": "Bag allowance", "ask": "How many kilos can I take on booking TN-48213?", "expect": "Says 7 kg of cabin baggage.", "tool_results": { "get_booking": { "cabin_kg": 7 } } }] }' # Read the file and its versions. The file comes back as YAML. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Roll back to the previous version. The version that was live before comes back at once. This needs a live key. curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/deployments/post-v1-deployments-interview.md --- # POST /v1/deployments/interview > Build a deployment by talking to Bash: describe the job, answer its questions, receive a draft version. In [Deployments API](https://vestra.ai/developers/reference/api/deployments.md): How your Bash is set up, in versions: apply a whole vestra.yaml, promote, roll back, and the interview that drafts the file. Send `Authorization: Bearer vk_test_...` on every call. Build a deployment by talking to Bash: describe the job, answer its questions, receive a draft version. Your API key, scope `setup` or `all`. On the card: [`init`](https://vestra.ai/developers/init.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `name` (`string`, required): The name of the new deployment the interview makes, 1 to 120 characters. It is always new: a taken name gets a numbered slug, and a taken `slug` is refused with 409 `slug_taken`. Limits: 1 to 120 characters. Example: `Trip rescue`. - `slug` (`string`): Its name in URLs and refs. Absent: made from `name`. Example: `trip-rescue`. - `job` (`string`, required): What Bash should do for your users, in your own words, 20 to 4,000 characters. Bash asks what it needs, then drafts the file. Limits: 20 to 4000 characters. Example: `Help Tripnest travellers fix broken trips: rebook, refund, find a hotel.`. ## Answers 202 - `object` (`"deployment_interview"`): Always `deployment_interview`. - `deployment` (`object`): The deployment the interview drafts. - `id` (`string`): The deployment's id. Example: `dep_8f2k`. - `object` (`"deployment"`): Always `deployment`. - `name` (`string`): Its name, as people read it. - `slug` (`string`): Its name in URLs and refs: send it where a route takes `{deployment_id}`. Example: `trip-rescue`. - `description` (`string`): What it is for; empty when none was given. - `base` (`"bash"`): What it is built on: always `bash`. - `status` (`"active" | "archived"`): `active`, or `archived` (put away: no new conversation or version reaches it). - `draft_version` (`integer`): The version test keys run. A new save moves it there; a promote with a test key moves it to the version named, older ones included. - `live_version` (`integer | null`): The version live keys run; null until one is promoted. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `conversation_id` (`string`): The interview's conversation: follow its events for Bash's questions, and answer with POST /v1/conversations/{conversation_id}/messages. ## Code **TypeScript** ```ts const draft = vestra.init({ describe: 'Help Tripnest travellers fix broken trips.' }); await draft.started; ``` **Python** ```python draft = vestra.init("Help Tripnest travellers fix broken trips.") started = draft.started ``` **CLI** ```bash vestra init "Help Tripnest travellers fix broken trips." --answers answers.txt ``` **HTTP** ```bash # Describe the job. The interview runs as a conversation, so its questions arrive like any other reply. curl "$VESTRA_BASE_URL/v1/deployments/interview" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Trip rescue", "slug": "trip-rescue", "job": "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking." }' # Answer its questions. Follow the conversation the response names, and reply in it. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_iv7/events" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/conversations/conv_iv7/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Ask first before paying a fare difference." }' # Save the draft to your repo. When the interview ends, download the draft as vestra.yaml, starter tests # included. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o vestra.yaml # Read the closing words. The draft version (the deployment's draft_version) has them in its interview field: # what it set up, the tools your code must answer, the spending cap per task, the tests, and the next steps. curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue/versions/2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "init", "arguments": { "describe": "Help Tripnest travellers fix broken trips: cancellations, delays, missed connections. Look bookings up with get_booking.", "for": "customers" } } } ``` --- url: https://vestra.ai/developers/reference/api/tests.md --- # Tests API > The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/deployments/{deployment_id}/scenarios](https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-scenarios.md): The deployment's test scenarios. - `POST` [/v1/deployments/{deployment_id}/scenarios](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios.md): Add one: a pretend user, their goal, what success looks like, what must never happen. - `POST` [/v1/deployments/{deployment_id}/scenarios/generate](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios-generate.md): Have Bash draft scenarios from the deployment's instructions. - `PATCH` [/v1/deployments/{deployment_id}/scenarios/{scenario_id}](https://vestra.ai/developers/reference/api/tests/patch-v1-deployments-deployment-id-scenarios-scenario-id.md): Change one. - `DELETE` [/v1/deployments/{deployment_id}/scenarios/{scenario_id}](https://vestra.ai/developers/reference/api/tests/delete-v1-deployments-deployment-id-scenarios-scenario-id.md): Remove one. - `POST` [/v1/deployments/{deployment_id}/test-runs](https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-test-runs.md): Run every scenario against a version: version, or the latest when left out. - `GET` [/v1/deployments/{deployment_id}/test-runs](https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-test-runs.md): Past test runs. - `GET` [/v1/test-runs/{test_run_id}](https://vestra.ai/developers/reference/api/tests/get-v1-test-runs-test-run-id.md): Pass or fail per scenario, the conversation and the judge's reason. --- url: https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-scenarios.md --- # GET /v1/deployments/{deployment_id}/scenarios > The deployment's test scenarios. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. The deployment's test scenarios. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. ## Answers 200 - `data` (`object[]`): The items on this page. - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`, may be absent): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`, may be absent): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. - `id` (`string`): The scenario's id. - `object` (`"scenario"`): Always `scenario`. - `deployment_id` (`string`): The deployment it tests. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/scenarios" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios.md --- # POST /v1/deployments/{deployment_id}/scenarios > Add one: a pretend user, their goal, what success looks like, what must never happen. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Add one: a pretend user, their goal, what success looks like, what must never happen. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `name` (`string`, required): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`, required): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`, required): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`, required): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. ## Answers 201 - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`, may be absent): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`, may be absent): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. - `id` (`string`): The scenario's id. - `object` (`"scenario"`): Always `scenario`. - `deployment_id` (`string`): The deployment it tests. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/scenarios" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Bag allowance", "persona": "A first-time flyer, anxious, on a phone.", "goal": "How many kilos can I take on booking TN-48213?", "success_criteria": [ "Says 7 kg of cabin baggage." ] }' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-scenarios-generate.md --- # POST /v1/deployments/{deployment_id}/scenarios/generate > Have Bash draft scenarios from the deployment's instructions. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Have Bash draft scenarios from the deployment's instructions. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, if you send one: - `count` (`integer`): How many tests to draft, 1 to 10. Default 5. Default: `5`. Limits: 1 to 10. - `version` (`integer`): The version whose instructions they are drafted from. Absent: the newest. Limits: at least 1. ## Answers 200 - `object` (`"list"`): Always `list`. - `data` (`object[]`): The drafted tests. They are not saved: save the ones you want, or write them into vestra.yaml. - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`, may be absent): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`, may be absent): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. ## Code **TypeScript** ```ts const drafted = await vestra.test.generate('vestra.yaml'); ``` **Python** ```python drafted = vestra.test.generate("vestra.yaml") ``` **CLI** ```bash vestra test --generate ``` **HTTP** ```bash # Draft more tests. The tests in your file are the deployment's scenarios, and Bash can draft more from your # instructions. curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' # Run the tests. This plays every scenario against the latest version, by the deployment's name. Then read # each verdict. If a restart cuts a run off, it resumes on its own: resumes says how often, and error_code says # why a run ended in error. curl -X POST "$VESTRA_BASE_URL/v1/deployments/trip-rescue/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_2k1" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "test", "arguments": { "deployment": "trip-rescue" } } } ``` --- url: https://vestra.ai/developers/reference/api/tests/patch-v1-deployments-deployment-id-scenarios-scenario-id.md --- # PATCH /v1/deployments/{deployment_id}/scenarios/{scenario_id} > Change one. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Change one. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. - `scenario_id` (`string`, required): The test scenario's id (`scn_...`). Body: - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. ## Answers 200 - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`, may be absent): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`, may be absent): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. - `id` (`string`): The scenario's id. - `object` (`"scenario"`): Always `scenario`. - `deployment_id` (`string`): The deployment it tests. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **HTTP** ```bash curl -X PATCH "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/scenarios/{scenario_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/tests/delete-v1-deployments-deployment-id-scenarios-scenario-id.md --- # DELETE /v1/deployments/{deployment_id}/scenarios/{scenario_id} > Remove one. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Remove one. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. - `scenario_id` (`string`, required): The test scenario's id (`scn_...`). ## Answers 200 - `id` (`string`): The deleted scenario's id. - `object` (`"scenario"`): Always `scenario`. - `deleted` (`true`): Always true: no test run plays it again. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/scenarios/{scenario_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/tests/post-v1-deployments-deployment-id-test-runs.md --- # POST /v1/deployments/{deployment_id}/test-runs > Run every scenario against a version: version, or the latest when left out. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Run every scenario against a version: version, or the latest when left out. The deployment is named by its id or its name. user_secrets sets the test user's own secrets for your MCP servers, replacing any an earlier run set. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, if you send one: - `version` (`integer`): The version to test. Absent: the newest. Limits: at least 1. - `user_secrets` (`map of string`): The test user's own secrets, by name, at most 20, for headers such as `${user_secret:NAME}`. They replace the whole set: `{}` clears it, and a run that sends none keeps the last. Never returned. Example: `{"AGENCY_ID":"..."}`. ## Answers 202 - `id` (`string`): The test run's id. Example: `trun_8f2k`. - `object` (`"test_run"`): Always `test_run`. - `deployment_id` (`string`): The deployment it tests. - `version` (`integer`): The version it tests. - `status` (`"queued" | "running" | "passed" | "failed" | "error"`): `queued` or `running` while it plays. `passed`: every scenario passed. `failed`: one or more did not. `error`: it could not finish, as `error_code` says. - `error_code` (`string | null`): Why it ended in `error`: `interrupted` (a restart stopped it too often), `test_wallet_empty`, `judge_unreadable` (the grader's answer could not be read) or `internal`. Null otherwise. - `scenario_count` (`integer`): How many scenarios it plays. - `passed_count` (`integer`): How many have passed so far. - `resumes` (`integer`): How many times a restart interrupted it and it played on from where it was, at most 2. - `promoted` (`object | null`): What its pass promoted, for an apply that promotes after its tests; null when it promoted nothing (none asked, refused, or it did not pass). - `mode` (`"live" | "test"`): The mode the version went live in. - `version` (`integer`): The version that went live. - `became_default` (`boolean`): True when that promotion made the deployment the company's default: an ask with no deployment now reaches it. - `cost_usd` (`number`): What the run cost, in billed US dollars. - `created_at` (`string`): When it was asked for, as an ISO 8601 date and time in UTC. - `started_at` (`string | null`): When it began to play; null while queued. - `finished_at` (`string | null`): When it ended; null while it plays. - `results` (`object[]`, may be absent): Each scenario's result, on GET /v1/test-runs/{test_run_id}; absent in lists. - `object` (`"test_result"`): Always `test_result`. - `scenario_id` (`string | null`): The scenario it played; null when the scenario was deleted since. - `scenario` (`object`): The scenario as it was played. - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`, may be absent): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`, may be absent): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. - `verdict` (`"passed" | "failed" | "error"`): `passed`: every line of success met and nothing it must never do. `failed`: not so. `error`: it could not be played to the end. - `reason` (`string`): Why, in a sentence or two. - `criteria` (`object[]`): Each line of success, graded. - `criterion` (`string`): One line of what a pass looks like. - `met` (`boolean`): Whether the conversation met it. - `evidence` (`string`): Why the grader says so, from the conversation. - `must_never` (`object[]`): Each thing it must never do, graded. - `rule` (`string`): One line of what must never happen. - `violated` (`boolean`): Whether it happened. - `evidence` (`string`): Why the grader says so, from the conversation. - `approvals_asked` (`object[]`): What Bash asked the test user's yes for, and what it said. - `kind` (`string`): The kind of ask Bash raised, such as `hands_approval`, `credit_approval` or `tool_permission`. - `category` (`"payment" | "account_action" | "connect_app" | "action" | "custom"`): What it would have done: a `payment`, an `account_action`, `connect_app`, another `action`, or `custom`. - `summary` (`string`): What it asked to do, in its words. - `decision` (`"denied" | "approved"`): What the test user said. It denies anything that would act for it, so a test never moves money or changes an account; a test with approves: true approves the tools of the deployment's own MCP servers, which a test plays from its tool_results or does not run. - `transcript` (`object[]`): The conversation, line by line. - `role` (`"customer" | "bash" | "note"`): Who said it: `customer` (the test user), `bash`, or a `note` on what happened (work started, a tool called). - `text` (`string`): What was said, or what happened. - `conversation_id` (`string | null`): The test conversation; null when it never started. - `cost_usd` (`number`): What playing it cost, in billed US dollars. ## Code **TypeScript** ```ts const summary = await vestra.test('vestra.yaml'); ``` **Python** ```python summary = vestra.test("vestra.yaml") ``` **CLI** ```bash vestra test ``` **HTTP** ```bash # Draft more tests. The tests in your file are the deployment's scenarios, and Bash can draft more from your # instructions. curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' # Run the tests. This plays every scenario against the latest version, by the deployment's name. Then read # each verdict. If a restart cuts a run off, it resumes on its own: resumes says how often, and error_code says # why a run ended in error. curl -X POST "$VESTRA_BASE_URL/v1/deployments/trip-rescue/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_2k1" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "test", "arguments": { "deployment": "trip-rescue" } } } ``` --- url: https://vestra.ai/developers/reference/api/tests/get-v1-deployments-deployment-id-test-runs.md --- # GET /v1/deployments/{deployment_id}/test-runs > Past test runs. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Past test runs. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The test run's id. Example: `trun_8f2k`. - `object` (`"test_run"`): Always `test_run`. - `deployment_id` (`string`): The deployment it tests. - `version` (`integer`): The version it tests. - `status` (`"queued" | "running" | "passed" | "failed" | "error"`): `queued` or `running` while it plays. `passed`: every scenario passed. `failed`: one or more did not. `error`: it could not finish, as `error_code` says. - `error_code` (`string | null`): Why it ended in `error`: `interrupted` (a restart stopped it too often), `test_wallet_empty`, `judge_unreadable` (the grader's answer could not be read) or `internal`. Null otherwise. - `scenario_count` (`integer`): How many scenarios it plays. - `passed_count` (`integer`): How many have passed so far. - `resumes` (`integer`): How many times a restart interrupted it and it played on from where it was, at most 2. - `promoted` (`object | null`): What its pass promoted, for an apply that promotes after its tests; null when it promoted nothing (none asked, refused, or it did not pass). - `mode` (`"live" | "test"`): The mode the version went live in. - `version` (`integer`): The version that went live. - `became_default` (`boolean`): True when that promotion made the deployment the company's default: an ask with no deployment now reaches it. - `cost_usd` (`number`): What the run cost, in billed US dollars. - `created_at` (`string`): When it was asked for, as an ISO 8601 date and time in UTC. - `started_at` (`string | null`): When it began to play; null while queued. - `finished_at` (`string | null`): When it ended; null while it plays. - `results` (`object[]`, may be absent): Each scenario's result, on GET /v1/test-runs/{test_run_id}; absent in lists. - `object` (`"test_result"`): Always `test_result`. - `scenario_id` (`string | null`): The scenario it played; null when the scenario was deleted since. - `scenario` (`object`): The scenario as it was played. - `verdict` (`"passed" | "failed" | "error"`): `passed`: every line of success met and nothing it must never do. `failed`: not so. `error`: it could not be played to the end. - `reason` (`string`): Why, in a sentence or two. - `criteria` (`object[]`): Each line of success, graded. - `must_never` (`object[]`): Each thing it must never do, graded. - `approvals_asked` (`object[]`): What Bash asked the test user's yes for, and what it said. - `transcript` (`object[]`): The conversation, line by line. - `conversation_id` (`string | null`): The test conversation; null when it never started. - `cost_usd` (`number`): What playing it cost, in billed US dollars. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/deployments/{deployment_id}/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/tests/get-v1-test-runs-test-run-id.md --- # GET /v1/test-runs/{test_run_id} > Pass or fail per scenario, the conversation and the judge's reason. In [Tests API](https://vestra.ai/developers/reference/api/tests.md): The scenarios a version must pass, drafted or written, and the runs that check it before your users see it. Send `Authorization: Bearer vk_test_...` on every call. Pass or fail per scenario, the conversation and the judge's reason. Your API key, scope `setup` or `all`. On the card: [`test`](https://vestra.ai/developers/test.md). Every call here is a checked sample. ## Request Path: - `test_run_id` (`string`, required): The test run's id (`trun_...`). ## Answers 200 - `id` (`string`): The test run's id. Example: `trun_8f2k`. - `object` (`"test_run"`): Always `test_run`. - `deployment_id` (`string`): The deployment it tests. - `version` (`integer`): The version it tests. - `status` (`"queued" | "running" | "passed" | "failed" | "error"`): `queued` or `running` while it plays. `passed`: every scenario passed. `failed`: one or more did not. `error`: it could not finish, as `error_code` says. - `error_code` (`string | null`): Why it ended in `error`: `interrupted` (a restart stopped it too often), `test_wallet_empty`, `judge_unreadable` (the grader's answer could not be read) or `internal`. Null otherwise. - `scenario_count` (`integer`): How many scenarios it plays. - `passed_count` (`integer`): How many have passed so far. - `resumes` (`integer`): How many times a restart interrupted it and it played on from where it was, at most 2. - `promoted` (`object | null`): What its pass promoted, for an apply that promotes after its tests; null when it promoted nothing (none asked, refused, or it did not pass). - `mode` (`"live" | "test"`): The mode the version went live in. - `version` (`integer`): The version that went live. - `became_default` (`boolean`): True when that promotion made the deployment the company's default: an ask with no deployment now reaches it. - `cost_usd` (`number`): What the run cost, in billed US dollars. - `created_at` (`string`): When it was asked for, as an ISO 8601 date and time in UTC. - `started_at` (`string | null`): When it began to play; null while queued. - `finished_at` (`string | null`): When it ended; null while it plays. - `results` (`object[]`, may be absent): Each scenario's result, on GET /v1/test-runs/{test_run_id}; absent in lists. - `object` (`"test_result"`): Always `test_result`. - `scenario_id` (`string | null`): The scenario it played; null when the scenario was deleted since. - `scenario` (`object`): The scenario as it was played. - `name` (`string`): Its name, 1 to 120 characters. Limits: 1 to 120 characters. Example: `Bag allowance`. - `persona` (`string`): Who the test user is, in a few sentences, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `A first-time flyer, anxious, on a phone.`. - `goal` (`string`): What the test user wants, in their words, up to 2,000 characters. Limits: 1 to 2000 characters. Example: `How many kilos can I take on booking TN-48213?`. - `success_criteria` (`string[]`): What a pass looks like: 1 to 12 lines, each up to 500 characters. Every one must be met. Limits: 1 to 12 items. Example: `["Says 7 kg of cabin baggage."]`. - `must_never` (`string[]`): What must never happen: up to 12 lines, each up to 500 characters. Any one fails the test. Default: `[]`. Limits: at most 12 items. Example: `["Promises a refund."]`. - `tool_results` (`map of any`, may be absent): What each tool answers in this test, as any JSON, at most 20: a declared tool by its name, and a tool of an MCP server as . (the server's slug), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. Example: `{"get_booking":{"cabin_kg":7}}`. - `approves` (`boolean`, may be absent): true: the test user says yes to each approval for a tool of one of the deployment's own MCP servers, which in a test plays its tool_results entry or does not run. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. Example: `true`. - `verdict` (`"passed" | "failed" | "error"`): `passed`: every line of success met and nothing it must never do. `failed`: not so. `error`: it could not be played to the end. - `reason` (`string`): Why, in a sentence or two. - `criteria` (`object[]`): Each line of success, graded. - `criterion` (`string`): One line of what a pass looks like. - `met` (`boolean`): Whether the conversation met it. - `evidence` (`string`): Why the grader says so, from the conversation. - `must_never` (`object[]`): Each thing it must never do, graded. - `rule` (`string`): One line of what must never happen. - `violated` (`boolean`): Whether it happened. - `evidence` (`string`): Why the grader says so, from the conversation. - `approvals_asked` (`object[]`): What Bash asked the test user's yes for, and what it said. - `kind` (`string`): The kind of ask Bash raised, such as `hands_approval`, `credit_approval` or `tool_permission`. - `category` (`"payment" | "account_action" | "connect_app" | "action" | "custom"`): What it would have done: a `payment`, an `account_action`, `connect_app`, another `action`, or `custom`. - `summary` (`string`): What it asked to do, in its words. - `decision` (`"denied" | "approved"`): What the test user said. It denies anything that would act for it, so a test never moves money or changes an account; a test with approves: true approves the tools of the deployment's own MCP servers, which a test plays from its tool_results or does not run. - `transcript` (`object[]`): The conversation, line by line. - `role` (`"customer" | "bash" | "note"`): Who said it: `customer` (the test user), `bash`, or a `note` on what happened (work started, a tool called). - `text` (`string`): What was said, or what happened. - `conversation_id` (`string | null`): The test conversation; null when it never started. - `cost_usd` (`number`): What playing it cost, in billed US dollars. ## Code **TypeScript** ```ts const run = await vestra.test.wait('trun_5kd2'); ``` **Python** ```python run = vestra.test.wait("trun_5kd2") ``` **CLI** ```bash vestra events trun_5kd2 ``` **HTTP** ```bash # Draft more tests. The tests in your file are the deployment's scenarios, and Bash can draft more from your # instructions. curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/scenarios/generate" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "count": 3 }' # Run the tests. This plays every scenario against the latest version, by the deployment's name. Then read # each verdict. If a restart cuts a run off, it resumes on its own: resumes says how often, and error_code says # why a run ended in error. curl -X POST "$VESTRA_BASE_URL/v1/deployments/trip-rescue/test-runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/test-runs/trun_2k1" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "test", "arguments": { "deployment": "trip-rescue" } } } ``` --- url: https://vestra.ai/developers/reference/api/computer-setup.md --- # Computer setup API > Wake a user's computer before they need it. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/users/{user_id}/warm](https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-warm.md): Wake the user's computer now (for example when they open your app), so their next task starts without waiting. - `POST` [/v1/users/{user_id}/browser/fetch](https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-browser-fetch.md): Open one page in a background tab of the user's own browser, with no model, and get back the bodies of the page's own data calls that match capture. - `GET` [/v1/users/{user_id}/browser/cookies](https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-cookies.md): Which cookies of some names the user's browser holds for one site, and until when: names, sites and expiry only, never a value. - `GET` [/v1/users/{user_id}/browser/watch](https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-watch.md): Watch the browser fetch's tabs live, as text/event-stream. --- url: https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-warm.md --- # POST /v1/users/{user_id}/warm > Wake the user's computer now (for example when they open your app), so their next task starts without waiting. In [Computer setup API](https://vestra.ai/developers/reference/api/computer-setup.md): Wake a user's computer before they need it. Send `Authorization: Bearer vk_test_...` on every call. Wake the user's computer now (for example when they open your app), so their next task starts without waiting. Answers 200 when it is already up and 202 while it wakes. At most 10 computers wake at once per company (429 warm_limit), and a second call for one user within a minute rides the first. keep_ms keeps it awake that long once it is up, up to two minutes; calling again extends it. Your API key, scope `runtime` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Every call here is a checked sample. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. Body, if you send one: - `keep_ms` (`integer`): Keep the computer awake this long once it is up, in ms: 1000 to 120000. Absent: it sleeps again soon after if nothing uses it. Calling warm again extends it. Limits: 1000 to 120000. ## Answers 200 or 202 - `user_id` (`string`): Your id for the user whose computer it is. - `status` (`"ready" | "waking"`): `ready`: the computer is up. `waking`: a wake started now; their next task starts without waiting once it lands. ## Code **TypeScript** ```ts await vestra.user('traveller-42').warm(); ``` **Python** ```python vestra.user("traveller-42").warm() ``` **CLI** ```bash vestra users warm traveller-42 ``` **HTTP** ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` --- url: https://vestra.ai/developers/reference/api/computer-setup/post-v1-users-user-id-browser-fetch.md --- # POST /v1/users/{user_id}/browser/fetch > Open one page in a background tab of the user's own browser, with no model, and get back the bodies of the page's own data calls that match capture. In [Computer setup API](https://vestra.ai/developers/reference/api/computer-setup.md): Wake a user's computer before they need it. Send `Authorization: Bearer vk_test_...` on every call. Open one page in a background tab of the user's own browser, with no model, and get back the bodies of the page's own data calls that match capture. Only on sites the user's deployment lists in browser.allowed_domains (403 domain_not_allowed), and never on a page in its browser.blocked_paths, such as a payment page (403 page_blocked). Wakes the computer when it sleeps. Answers as soon as every required capture has its responses, with timings; 409 browser_in_use while a person drives the browser, 409 browser_busy past four at once; 504 timeout carries what came back in partial. frames returns small pictures of the tab at each milestone. A page_fetch step calls one of the deployment's sites from inside the page, as the page's own script would; keep_open_ms keeps the tab for the next fetch, which sends no url (409 no_open_tab when none is kept). Your API key, scope `runtime` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Every call here is a checked sample. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. Body: - `url` (`string (URL)`): The page to open, http or https, on a site the user's deployment allows (browser.allowed_domains). Absent: the steps run on the tab an earlier fetch kept open (keep_open_ms), as it is; 409 no_open_tab when none is kept. Limits: at most 4000 characters. Example: `https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=4&class=e`. - `capture` (`object[]`): The responses to keep, up to 10. The fetch answers as soon as every step is done and every required capture has its responses. Absent: none, for a fetch made of page_fetch steps. Limits: at most 10 items. - `match` (`string`, required): Part of the address of a response to keep, such as `/api/v3/search`: a finished response whose address contains it is kept. Ending in `$`, it must end the path instead (the query aside), so `/v3/services$` keeps `/v3/services` and not `/v3/services/meta/105`. Limits: 1 to 500 characters. Example: `/flights/v2/search/stream`. - `required` (`boolean`): Whether the fetch waits for it. Default true: the fetch answers as soon as every required capture has min_count responses. false: kept if it comes before then, never waited for. - `min_count` (`integer`): How many responses a required capture waits for, 1 to 10. Default 1. A page that loads its second page by itself (hotels) needs 2. Limits: 1 to 10. - `max_count` (`integer`): The most responses it keeps, 1 to 10. Default min_count, or 1. Limits: 1 to 10. - `max_bytes` (`integer`): The most bytes of each body to keep, up to 1048576, or up to 4194304 with sse "last". Default 524288; a longer body comes back cut, with truncated true. Limits: 1 to 4194304. - `sse` (`"last"`): For an event stream (text/event-stream) whose every event is the whole answer so far: "last" keeps only its last complete event, as a stream of that one event, and max_bytes is that event's limit. When the last event is past max_bytes, or the stream ends partway through one, the newest complete event that fits comes back instead, with truncated true. A body that is not an event stream is kept as it is. Absent: the body as it came. - `steps` (`object[]`): What to do on the page after it opens, in order, up to 20. Absent: nothing; the page is only opened. Limits: at most 20 items. - `frames` (`object`): Small pictures of the tab as it works: when the page first shows, when each required capture arrives, after each step, every 0.7 s, and at the end. At most 6 and about 400 KB in all. Absent: none. - `width` (`integer`): How wide each frame is, in pixels: 160 to 1280. Default 640. Limits: 160 to 1280. - `settle_ms` (`integer`): Once every required capture is in, keep taking frames, labelled `results`, about every 170 ms for this long while the page draws what came, then answer: 0 to 700 ms. A page can blank for a moment as it draws; a run of frames catches the drawn one. Default 0: answer at once, and the last frame may still show the page loading. Limits: 0 to 700. - `timeout_ms` (`integer`): How long the page may take, from when it starts opening, in ms: 1000 to 20000. Default 8000. Waking the computer comes on top. Limits: 1000 to 20000. - `grace_ms` (`integer`): Once every required capture has its min_count, wait up to this long for every capture to fill to its max_count, optional ones too, then answer with what came: 0 to 3000 ms. Default 0: answer at once. Limits: 0 to 3000. - `keep_open_ms` (`integer`): Keep this fetch's tab open on its page this long after it answers, with the computer awake, so the next fetch with no url runs there at once: 0 to 600000 ms. Default 0. Limits: 0 to 600000. - `tab` (`string`): Which kept tab: keep_open_ms keeps this tab under the name, and a fetch with no url takes the tab of that name. Up to 4 are kept. Default `default`. Example: `stays`. - `tag` (`string`): Your label for this fetch; the browser watch names its tab with it. - `front` (`boolean`): A hand-off the person finishes themselves, such as a sign-in: the tab's window opens in front of theirs, maximized, with the keyboard, and with keep_open_ms stays there for them. Default false: behind their window. - `front_device` (`"phone" | "desktop"`): With front: `phone` (default) or `desktop`, the site's desktop page, as the other fetches see it, at a phone's shape (440 wide, as high as the window): a sign-in service's tab it opens draws into the same part of the screen, so a session's screen shows one rectangle whatever is in front. - `front_crop` (`object`): On a hand-off (front, or a fetch with no url on its kept tab): a session's screen shows only this element of the page, such as a site's sign-in dialog at a phone's size. From a front fetch's start until the element is on the page, the screen answers `crop_pending` instead of a crop, for up to 20 seconds; past that it shows the page as before. - `role` (`string`): The element's accessibility role, such as `dialog`. Limits: 1 to 40 characters. Example: `dialog`. - `name` (`string`): Its accessible name, exactly. Limits: 1 to 200 characters. - `text` (`string`): Words its accessible name contains. Limits: 1 to 200 characters. - `stage` (`boolean`): In front, in one kept tab (`tab`), shown alone in the watch. - `private` (`boolean`): Never shown in the browser watch, and no frames taken: a person's own account page, say. ## Answers 200 - `final_url` (`string | null`): The page's address when the fetch ended, after any redirects and steps; null if it never opened. - `elapsed_ms` (`integer`): How long the whole call took, in ms, waking included. - `woke` (`boolean`): true when the computer was asleep and this call woke it. - `timings` (`object`): Where the time went, each in ms after the call arrived. - `wake_ms` (`integer`): How long waking the computer took: 0 when it was up. - `nav_ms` (`integer | null`): When the page started showing (its navigation committed); null if it never did, or when the fetch ran on a kept tab with nothing to open. - `first_capture_ms` (`integer | null`): When the first kept response finished; null if none did. - `done_ms` (`integer`): When the computer finished the fetch. - `captures` (`object[]`): Every response kept, in the order they finished. - `match` (`string`): The capture this response answers, or the page_fetch step's capture_as. - `url` (`string`): The full address of the response. - `status` (`integer`): Its HTTP status. - `content_type` (`string | null`): Its content type, such as application/json; null when it had none. - `body` (`string`): Its body, as text. - `truncated` (`boolean`): true when the body was longer than max_bytes, or the fetch had kept 8000000 bytes in all, and it was cut. With sse "last": true when the event kept is not the stream's last one (the last was too long, or the stream ended partway through it). - `at_ms` (`integer`): When it finished, in ms after the call arrived. - `frames` (`object[]`, may be absent): With `frames` asked: the pictures, in the order they were taken. A frame that would have slowed the answer is left out. - `label` (`string`): Which moment: `navigated`, `captured `, `after ` or `done`. Example: `navigated`. - `at_ms` (`integer`): When it was taken, in ms after the call arrived. - `page_url` (`string`): The page's address when it was taken; always on an allowed site. - `jpeg_base64` (`string`): The picture, a JPEG, as base64. - `step` (`string`, may be absent): With a `timeout`: where the fetch was when the time ran out: `navigate`, a step by its number and op such as `2 scroll`, or `captures` when every step was done and it was waiting for the required captures. Example: `2 scroll`. - `challenge` (`"turnstile"`, may be absent): `turnstile`: the page called Cloudflare's Turnstile check. ## Code **HTTP** ```bash # Create a user. Do it before their first ask to set their name and time zone. It is safe to repeat with the # same user_id. curl "$VESTRA_BASE_URL/v1/users" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "name": "Priya Sharma", "timezone": "Asia/Kolkata" }' # Read their spend. Their profile shows what they spent today and this month, against their caps. curl "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Set a monthly cap. This one is 20 US dollars. curl -X PATCH "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "monthly_cap_usd": 20 }' # Wake their computer. Do it before they need it. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/warm" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Open a page in their browser with no model, and keep the page's own data calls. The site must be in their # deployment's browser.allowed_domains. curl -X POST "$VESTRA_BASE_URL/v1/users/traveller-42/browser/fetch" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.ixigo.com/search/result/flight?from=DEL&to=GOI&date=06112026&adults=2&class=e", "capture": [{ "match": "/flights/v2/search/stream" }] }' # Erase a user. It runs as a job: the response is 202 with deleted false and status deleting, and then # user.deleted says it is done. curl -X DELETE "$VESTRA_BASE_URL/v1/users/traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "users_get", "arguments": { "user": "traveller-42" } } } ``` **TypeScript, Python, CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-cookies.md --- # GET /v1/users/{user_id}/browser/cookies > Which cookies of some names the user's browser holds for one site, and until when: names, sites and expiry only, never a value. In [Computer setup API](https://vestra.ai/developers/reference/api/computer-setup.md): Wake a user's computer before they need it. Send `Authorization: Bearer vk_test_...` on every call. Which cookies of some names the user's browser holds for one site, and until when: names, sites and expiry only, never a value. Only a site on the deployment's browser.allowed_domains (403 domain_not_allowed). Opens no page and acts on nothing, so it never disturbs a person using the browser: read it every few seconds to see a person finish signing in on a page you handed them (front), as the site's session cookie appears or its expiry moves on. Wakes the computer when it sleeps. Your API key, scope `runtime` or `all`. On the card: [`users`](https://vestra.ai/developers/users.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. Query: - `domain` (`string`, required): The site, a bare host name on the deployment's browser.allowed_domains, such as `ixigo.com`. Cookies of its subdomains count too. - `names` (`string`, required): The cookie names to look for, comma-separated, up to 10. ## Answers 200 - `cookies` (`object[]`): The cookies of those names the browser holds for the site and its subdomains. Never a value. Empty when it holds none. - `name` (`string`): The cookie's name, one of those asked for. - `domain` (`string`): The host the cookie is set for, with no leading dot. - `expires_at` (`string | null`): When it expires, as an ISO 8601 time; null for a cookie that lasts only while the browser runs. - `woke` (`boolean`): true when the computer was asleep and this call woke it. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/users/{user_id}/browser/cookies?domain=...&names=..." \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/computer-setup/get-v1-users-user-id-browser-watch.md --- # GET /v1/users/{user_id}/browser/watch > Watch the browser fetch's tabs live, as text/event-stream. In [Computer setup API](https://vestra.ai/developers/reference/api/computer-setup.md): Wake a user's computer before they need it. Send `Authorization: Bearer vk_test_...` on every call. Watch the browser fetch's tabs live, as text/event-stream. Read only: allowed sites only, a tab on a blocked path is hidden, and it never wakes the computer. Your API key, scope `runtime` or `all`. On the card: [`watch`](https://vestra.ai/developers/watch.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. Query: - `fps` (`integer`): Pictures a second, 1 to 10. - `width` (`integer`): Picture width, 320 to 1280. Headers: - `Last-Event-ID` (`string`): Resume after this event id. ## Answers 200 A stream of events; with format=json, a page of them. ## Code **HTTP** ```bash curl -N "$VESTRA_BASE_URL/v1/users/{user_id}/browser/watch" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/routines.md --- # Routines API > Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/routines](https://vestra.ai/developers/reference/api/routines/post-v1-routines.md): Create one: what to ask, and when: every (plain words such as "weekday at 08:00", read in the user's time zone), a cron schedule, or on "call" (your system posts to its trigger_url). - `GET` [/v1/routines](https://vestra.ai/developers/reference/api/routines/get-v1-routines.md): The routines of one user (user_id), or of the company's own Bash. - `GET` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id.md): One routine. - `PATCH` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/patch-v1-routines-routine-id.md): Change what it asks, when it runs (every, schedule or timezone), its filters, or pause and resume it. - `DELETE` [/v1/routines/{routine_id}](https://vestra.ai/developers/reference/api/routines/delete-v1-routines-routine-id.md): Delete it. - `POST` [/v1/routines/{routine_id}/run](https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-run.md): Run it now. - `POST` [/v1/routines/{routine_id}/trigger](https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-trigger.md): Fire it with a payload from your system: a train delayed, a refund posted. - `GET` [/v1/routines/{routine_id}/runs](https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id-runs.md): Past runs and the tasks they started. --- url: https://vestra.ai/developers/reference/api/routines/post-v1-routines.md --- # POST /v1/routines > Create one: what to ask, and when: every (plain words such as "weekday at 08:00", read in the user's time zone), a cron schedule, or on "call" (your system posts to its trigger_url). In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. Create one: what to ask, and when: every (plain words such as "weekday at 08:00", read in the user's time zone), a cron schedule, or on "call" (your system posts to its trigger_url). For a user, or with no user_id for the company's own Bash. Each run is a message in the routine's conversation. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `user_id` (`string`): Whose Bash runs it: your id for the user. Absent: your company's own Bash. Example: `traveller-42`. - `ask` (`string`, required): What Bash is asked on every run, as a person would ask it, up to 8,000 characters. Limits: 1 to 8000 characters. Example: `Check today's trips for delays and tell me what changed.`. - `name` (`string`): Its name, up to 200 characters. Absent: the start of `ask`. Limits: 1 to 200 characters. - `every` (`string`): When it runs, in plain words: `weekday at 08:00`, `mon, wed, fri at 18:30`, `30 minutes`, `month on the 1st at 09:00`. At most once every 15 minutes. Give one of `every`, `schedule` or `on`. Limits: 1 to 100 characters. Example: `weekday at 08:00`. - `timezone` (`string`): The zone `every` is read in, an IANA name such as `Asia/Kolkata`. Absent: the user's own, then UTC. Limits: 1 to 64 characters. Example: `Asia/Kolkata`. - `schedule` (`object`): When it runs, as cron, instead of `every`. - `cron` (`string`, required): A cron expression of five fields: `0 7 * * *` runs at 7:00 every day. Limits: 1 to 120 characters. Example: `0 7 * * 1-5`. - `timezone` (`string`): The zone the cron is read in, an IANA name such as `Asia/Kolkata`. Absent: the user's own, then UTC. Limits: 1 to 64 characters. Example: `Asia/Kolkata`. - `on` (`"call"`): `call`: it runs whenever your system posts to its `trigger_url`, instead of on a schedule. - `filters` (`map of any`): With `on: call`, the fields and values a payload must carry for the routine to run; any other payload is `filtered`. Example: `{"event":"flight.delayed"}`. - `conversation_id` (`string`): An existing conversation of this user that each run posts into. Absent: the routine gets its own. Limits: at least 1 character. - `deployment` (`string`): For the routine's own conversation, a deployment by its `dep_` id or name. Absent: the user's default, then the company's. Limits: at least 1 character. - `enabled` (`boolean`): false makes it paused: it keeps its schedule but does not run. Default true. Default: `true`. ## Answers 201 - `id` (`string`): The routine's id. Example: `rtn_8f2k`. - `object` (`"routine"`): Always `routine`. - `user_id` (`string | null`): Your id for the user whose Bash runs it; null for your company's own Bash. - `conversation_id` (`string | null`): The conversation each run posts into, named or made when the routine was; null for a routine not made through the API. - `name` (`string`): Its name. - `ask` (`string`): What Bash is asked on every run. - `kind` (`"schedule" | "trigger" | "app_event"`): `schedule`: it runs on its schedule. `trigger`: it runs when your system posts to `trigger_url`. `app_event`: an event in a connected app runs it. - `schedule` (`object | null`): When it runs; null for one that runs on a call. - `every` (`string | null`): The plain words it was given; null for a cron. - `cron` (`string`): The schedule as cron, worked out from `every` when it was given in words. - `timezone` (`string`): The zone it runs in, an IANA name. - `trigger` (`object | null`): For one that runs on a call, what it listens for; null for one on a schedule. - `filters` (`map of any | null`): What a payload must carry for it to run; null: any payload. - `trigger_url` (`string | null`): For one that runs on a call: POST here, with your key and any JSON object, to run it. Null otherwise. - `enabled` (`boolean`): false while it is paused. - `status` (`"active" | "completed" | "failed"`): `active`: it runs as it should. `completed`: it has finished and will not run again. `failed`: it stopped on an error. - `last_run_at` (`string | null`): When it last ran; null when it never has. - `next_run_at` (`string | null`): When it runs next; null when paused, finished, or run on a call. - `run_count` (`integer`): How many times it has run. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **TypeScript** ```ts const routine = await vestra.user('traveller-42').routines.create({ ask: 'Check my trips for today.', every: 'weekday at 08:00' }); ``` **Python** ```python routine = vestra.user("traveller-42").routines.create("Check my trips for today.", every="weekday at 08:00") ``` **CLI** ```bash vestra routines add "Check my trips for today." --every "weekday at 08:00" --user traveller-42 ``` **HTTP** ```bash # Run on a schedule. Write it in plain words, read in the user's own time zone. The response gives the cron # expression and the next run. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Run when your system calls. Post a payload to the routine's trigger_url. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' # Manage a routine. Pause it (enabled: true resumes it), run it once now, and list the user's routines. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' curl -X POST "$VESTRA_BASE_URL/v1/routines/rtn_5kd/run" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/routines?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` --- url: https://vestra.ai/developers/reference/api/routines/get-v1-routines.md --- # GET /v1/routines > The routines of one user (user_id), or of the company's own Bash. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. The routines of one user (user_id), or of the company's own Bash. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `user_id` (`string`): Whose routines: your id for the user. Absent: your company's own Bash's. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The routine's id. Example: `rtn_8f2k`. - `object` (`"routine"`): Always `routine`. - `user_id` (`string | null`): Your id for the user whose Bash runs it; null for your company's own Bash. - `conversation_id` (`string | null`): The conversation each run posts into, named or made when the routine was; null for a routine not made through the API. - `name` (`string`): Its name. - `ask` (`string`): What Bash is asked on every run. - `kind` (`"schedule" | "trigger" | "app_event"`): `schedule`: it runs on its schedule. `trigger`: it runs when your system posts to `trigger_url`. `app_event`: an event in a connected app runs it. - `schedule` (`object | null`): When it runs; null for one that runs on a call. - `every` (`string | null`): The plain words it was given; null for a cron. - `cron` (`string`): The schedule as cron, worked out from `every` when it was given in words. - `timezone` (`string`): The zone it runs in, an IANA name. - `trigger` (`object | null`): For one that runs on a call, what it listens for; null for one on a schedule. - `filters` (`map of any | null`): What a payload must carry for it to run; null: any payload. - `trigger_url` (`string | null`): For one that runs on a call: POST here, with your key and any JSON object, to run it. Null otherwise. - `enabled` (`boolean`): false while it is paused. - `status` (`"active" | "completed" | "failed"`): `active`: it runs as it should. `completed`: it has finished and will not run again. `failed`: it stopped on an error. - `last_run_at` (`string | null`): When it last ran; null when it never has. - `next_run_at` (`string | null`): When it runs next; null when paused, finished, or run on a call. - `run_count` (`integer`): How many times it has run. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const routines = await vestra.routines.list(); ``` **Python** ```python routines = vestra.routines.list() ``` **CLI** ```bash vestra routines ``` **HTTP** ```bash # Run on a schedule. Write it in plain words, read in the user's own time zone. The response gives the cron # expression and the next run. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Run when your system calls. Post a payload to the routine's trigger_url. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' # Manage a routine. Pause it (enabled: true resumes it), run it once now, and list the user's routines. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' curl -X POST "$VESTRA_BASE_URL/v1/routines/rtn_5kd/run" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/routines?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` --- url: https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id.md --- # GET /v1/routines/{routine_id} > One routine. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. One routine. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `routine_id` (`string`, required): The routine's id (`rtn_...`). ## Answers 200 - `id` (`string`): The routine's id. Example: `rtn_8f2k`. - `object` (`"routine"`): Always `routine`. - `user_id` (`string | null`): Your id for the user whose Bash runs it; null for your company's own Bash. - `conversation_id` (`string | null`): The conversation each run posts into, named or made when the routine was; null for a routine not made through the API. - `name` (`string`): Its name. - `ask` (`string`): What Bash is asked on every run. - `kind` (`"schedule" | "trigger" | "app_event"`): `schedule`: it runs on its schedule. `trigger`: it runs when your system posts to `trigger_url`. `app_event`: an event in a connected app runs it. - `schedule` (`object | null`): When it runs; null for one that runs on a call. - `every` (`string | null`): The plain words it was given; null for a cron. - `cron` (`string`): The schedule as cron, worked out from `every` when it was given in words. - `timezone` (`string`): The zone it runs in, an IANA name. - `trigger` (`object | null`): For one that runs on a call, what it listens for; null for one on a schedule. - `filters` (`map of any | null`): What a payload must carry for it to run; null: any payload. - `trigger_url` (`string | null`): For one that runs on a call: POST here, with your key and any JSON object, to run it. Null otherwise. - `enabled` (`boolean`): false while it is paused. - `status` (`"active" | "completed" | "failed"`): `active`: it runs as it should. `completed`: it has finished and will not run again. `failed`: it stopped on an error. - `last_run_at` (`string | null`): When it last ran; null when it never has. - `next_run_at` (`string | null`): When it runs next; null when paused, finished, or run on a call. - `run_count` (`integer`): How many times it has run. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **TypeScript** ```ts const routine = await vestra.routines.get('rtn_5kd'); ``` **Python** ```python routine = vestra.routines.get("rtn_5kd") ``` **CLI** ```bash vestra routines get rtn_5kd ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/routines/{routine_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/routines/patch-v1-routines-routine-id.md --- # PATCH /v1/routines/{routine_id} > Change what it asks, when it runs (every, schedule or timezone), its filters, or pause and resume it. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. Change what it asks, when it runs (every, schedule or timezone), its filters, or pause and resume it. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Every call here is a checked sample. ## Request Path: - `routine_id` (`string`, required): The routine's id (`rtn_...`). Body: - `name` (`string`): A new name, up to 200 characters. Limits: 1 to 200 characters. - `ask` (`string`): What Bash is asked on every run from now on, up to 8,000 characters. Limits: 1 to 8000 characters. - `every` (`string`): A new schedule. When it runs, in plain words: `weekday at 08:00`, `mon, wed, fri at 18:30`, `30 minutes`, `month on the 1st at 09:00`. At most once every 15 minutes. Limits: 1 to 100 characters. - `timezone` (`string`): A new zone, an IANA name, for the schedule it already has or for new plain words. Limits: 1 to 64 characters. - `schedule` (`object`): A new schedule as cron, instead of `every`. - `cron` (`string`, required): A cron expression of five fields: `0 7 * * *` runs at 7:00 every day. Limits: 1 to 120 characters. Example: `0 7 * * 1-5`. - `timezone` (`string`): The zone the cron is read in, an IANA name such as `Asia/Kolkata`. Absent: the user's own, then UTC. Limits: 1 to 64 characters. Example: `Asia/Kolkata`. - `filters` (`map of any`): New filters, for a routine that runs on a call. - `enabled` (`boolean`): false pauses it; true resumes it. ## Answers 200 - `id` (`string`): The routine's id. Example: `rtn_8f2k`. - `object` (`"routine"`): Always `routine`. - `user_id` (`string | null`): Your id for the user whose Bash runs it; null for your company's own Bash. - `conversation_id` (`string | null`): The conversation each run posts into, named or made when the routine was; null for a routine not made through the API. - `name` (`string`): Its name. - `ask` (`string`): What Bash is asked on every run. - `kind` (`"schedule" | "trigger" | "app_event"`): `schedule`: it runs on its schedule. `trigger`: it runs when your system posts to `trigger_url`. `app_event`: an event in a connected app runs it. - `schedule` (`object | null`): When it runs; null for one that runs on a call. - `every` (`string | null`): The plain words it was given; null for a cron. - `cron` (`string`): The schedule as cron, worked out from `every` when it was given in words. - `timezone` (`string`): The zone it runs in, an IANA name. - `trigger` (`object | null`): For one that runs on a call, what it listens for; null for one on a schedule. - `filters` (`map of any | null`): What a payload must carry for it to run; null: any payload. - `trigger_url` (`string | null`): For one that runs on a call: POST here, with your key and any JSON object, to run it. Null otherwise. - `enabled` (`boolean`): false while it is paused. - `status` (`"active" | "completed" | "failed"`): `active`: it runs as it should. `completed`: it has finished and will not run again. `failed`: it stopped on an error. - `last_run_at` (`string | null`): When it last ran; null when it never has. - `next_run_at` (`string | null`): When it runs next; null when paused, finished, or run on a call. - `run_count` (`integer`): How many times it has run. - `created_at` (`string`): When it was made, as an ISO 8601 date and time in UTC. - `updated_at` (`string`): When it last changed, as an ISO 8601 date and time in UTC. ## Code **TypeScript** ```ts await vestra.routines.pause('rtn_5kd'); ``` **Python** ```python vestra.routines.pause("rtn_5kd") ``` **CLI** ```bash vestra routines pause rtn_5kd ``` **HTTP** ```bash # Run on a schedule. Write it in plain words, read in the user's own time zone. The response gives the cron # expression and the next run. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Run when your system calls. Post a payload to the routine's trigger_url. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' # Manage a routine. Pause it (enabled: true resumes it), run it once now, and list the user's routines. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' curl -X POST "$VESTRA_BASE_URL/v1/routines/rtn_5kd/run" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/routines?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` --- url: https://vestra.ai/developers/reference/api/routines/delete-v1-routines-routine-id.md --- # DELETE /v1/routines/{routine_id} > Delete it. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. Delete it. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `routine_id` (`string`, required): The routine's id (`rtn_...`). ## Answers 200 - `id` (`string`): The deleted routine's id. - `object` (`"routine"`): Always `routine`. - `deleted` (`true`): Always true: it is gone and never runs again. ## Code **TypeScript** ```ts await vestra.routines.delete('rtn_5kd'); ``` **Python** ```python vestra.routines.delete("rtn_5kd") ``` **CLI** ```bash vestra routines delete rtn_5kd ``` **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/routines/{routine_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-run.md --- # POST /v1/routines/{routine_id}/run > Run it now. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. Run it now. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Every call here is a checked sample. ## Request Path: - `routine_id` (`string`, required): The routine's id (`rtn_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 202 - `object` (`"routine_fire"`): Always `routine_fire`. - `routine_id` (`string`): The routine it runs. - `status` (`"queued" | "duplicate" | "filtered"`): `queued`: it runs now. `duplicate`: the same run was already asked for, so it runs once. `filtered`: the payload did not match its `filters`, so it does not run. ## Code **TypeScript** ```ts await vestra.routines.run('rtn_5kd'); ``` **Python** ```python vestra.routines.run("rtn_5kd") ``` **CLI** ```bash vestra routines run rtn_5kd ``` **HTTP** ```bash # Run on a schedule. Write it in plain words, read in the user's own time zone. The response gives the cron # expression and the next run. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Run when your system calls. Post a payload to the routine's trigger_url. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' # Manage a routine. Pause it (enabled: true resumes it), run it once now, and list the user's routines. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' curl -X POST "$VESTRA_BASE_URL/v1/routines/rtn_5kd/run" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/routines?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` --- url: https://vestra.ai/developers/reference/api/routines/post-v1-routines-routine-id-trigger.md --- # POST /v1/routines/{routine_id}/trigger > Fire it with a payload from your system: a train delayed, a refund posted. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. Fire it with a payload from your system: a train delayed, a refund posted. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Every call here is a checked sample. ## Request Path: - `routine_id` (`string`, required): The routine's id (`rtn_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body, if you send one: `map of any` ## Answers 200 or 202 - `object` (`"routine_fire"`): Always `routine_fire`. - `routine_id` (`string`): The routine it runs. - `status` (`"queued" | "duplicate" | "filtered"`): `queued`: it runs now. `duplicate`: the same run was already asked for, so it runs once. `filtered`: the payload did not match its `filters`, so it does not run. ## Code **TypeScript** ```ts await vestra.routines.trigger('rtn_7qp'); ``` **Python** ```python vestra.routines.trigger("rtn_7qp") ``` **HTTP** ```bash # Run on a schedule. Write it in plain words, read in the user's own time zone. The response gives the cron # expression and the next run. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00" }' # Run when your system calls. Post a payload to the routine's trigger_url. curl "$VESTRA_BASE_URL/v1/routines" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "ask": "A train of mine is late. Find me another way.", "on": "call" }' curl "$VESTRA_BASE_URL/v1/routines/rtn_7qp/trigger" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "train": "12127", "delay_minutes": 45 }' # Manage a routine. Pause it (enabled: true resumes it), run it once now, and list the user's routines. curl -X PATCH "$VESTRA_BASE_URL/v1/routines/rtn_5kd" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' curl -X POST "$VESTRA_BASE_URL/v1/routines/rtn_5kd/run" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/routines?user_id=traveller-42" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "routines_add", "arguments": { "user": "traveller-42", "ask": "Check my trips for today and tell me about any delay.", "every": "weekday at 08:00", "timezone": "Asia/Kolkata" } } } ``` **CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/routines/get-v1-routines-routine-id-runs.md --- # GET /v1/routines/{routine_id}/runs > Past runs and the tasks they started. In [Routines API](https://vestra.ai/developers/reference/api/routines.md): Jobs Bash runs on a schedule in plain words, or whenever your system calls. Send `Authorization: Bearer vk_test_...` on every call. Past runs and the tasks they started. Your API key, scope `runtime` or `all`. On the card: [`routines`](https://vestra.ai/developers/routines.md). Every call here is a checked sample. ## Request Path: - `routine_id` (`string`, required): The routine's id (`rtn_...`). Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The run's id. - `object` (`"routine_run"`): Always `routine_run`. - `routine_id` (`string`): The routine it is a run of. - `conversation_id` (`string | null`): The conversation the run's message landed in; null when it never got that far. - `status` (`"pending" | "running" | "completed" | "failed" | "skipped"`): `pending` or `running` while it goes. `completed`: Bash took the ask (the work it started is in `task_ids`). `failed`: it could not post the ask. `skipped`: it did not run, as `error` says. - `trigger_payload` (`map of any | null`): The payload your system sent, for a run on a call; null for a scheduled run. - `task_ids` (`string[]`): The pieces of work the run started. - `error` (`string | null`): Why it failed or was skipped; null otherwise. - `started_at` (`string`): When it began, as an ISO 8601 date and time in UTC. - `completed_at` (`string | null`): When it ended; null while it goes. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const runs = await vestra.routines.runs('rtn_5kd'); ``` **Python** ```python runs = vestra.routines.runs("rtn_5kd") ``` **CLI** ```bash vestra routines runs rtn_5kd ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/routines/rtn_5kd/runs" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/batches.md --- # Batches API > Many users at once: one ask each, followed together. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/batches](https://vestra.ai/developers/reference/api/batches/post-v1-batches.md): Start a batch: a deployment, a list of items (user_id optional, message, context, metadata) and a concurrency. - `GET` [/v1/batches/{batch_id}](https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id.md): Progress: counts by status, spend so far. - `GET` [/v1/batches/{batch_id}/items](https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id-items.md): Each item with its task id and result. - `POST` [/v1/batches/{batch_id}/cancel](https://vestra.ai/developers/reference/api/batches/post-v1-batches-batch-id-cancel.md): Stop starting new items; running ones finish. --- url: https://vestra.ai/developers/reference/api/batches/post-v1-batches.md --- # POST /v1/batches > Start a batch: a deployment, a list of items (user_id optional, message, context, metadata) and a concurrency. In [Batches API](https://vestra.ai/developers/reference/api/batches.md): Many users at once: one ask each, followed together. Send `Authorization: Bearer vk_test_...` on every call. Start a batch: a deployment, a list of items (user_id optional, message, context, metadata) and a concurrency. Each item is one conversation message with hand_off: always. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. Body: - `deployment` (`string`): Which deployment answers every item: its id or name, never a pinned version. Absent: your company's default deployment. Limits: at least 1 character. - `items` (`object[]`, required): The jobs: 1 to 5,000, each one conversation. Limits: 1 to 5000 items. - `user_id` (`string`): Your id for the user this item is for. Absent: your company's own Bash. Example: `traveller-42`. - `message` (`string`, required): What Bash is asked, in at most 32,000 characters. It goes straight to work. Limits: 1 to 32000 characters. Example: `Is your flight hit by the storm?`. - `context` (`map of any`): Facts for this item only, as JSON: Bash reads them as data, never as instructions. - `metadata` (`map of string`): Your own labels, given back as you sent them: at most 32 keys of 1 to 64 characters, each value a string of at most 512. Bash never reads them. Example: `{"order_id":"ORD-1042","channel":"app"}`. - `concurrency` (`integer`): How many items run at once, 1 to 100. Default: 10. Items with no user share your company's own Bash, 5 at a time at most. Default: `10`. Limits: 1 to 100. - `metadata` (`map of string`): Your own labels, given back as you sent them: at most 32 keys of 1 to 64 characters, each value a string of at most 512. Bash never reads them. Example: `{"order_id":"ORD-1042","channel":"app"}`. ## Answers 201 - `id` (`string`): The batch (bat_...). Example: `bat_7xk`. - `object` (`"batch"`): Always batch. - `status` (`"running" | "cancelled" | "completed"`): running: items are still starting or running. cancelled: you cancelled it, so no new item starts. completed: every item has finished. - `deployment_id` (`string | null`): The deployment that answers its items (dep_...); null when none applies. - `concurrency` (`integer`): How many items run at once. - `item_count` (`integer`): How many items it has. - `counts` (`object`): Its items, by status. - `queued` (`integer`): Items not started yet. - `running` (`integer`): Items running now. - `succeeded` (`integer`): Items that finished well. - `failed` (`integer`): Items that failed. - `cancelled` (`integer`): Items a cancel kept from starting. - `spend_usd` (`number`): What it has cost so far, in billed US dollars: every item's conversation and all its work. Example: `0.061`. - `metadata` (`map of string`): Your labels, as you sent them; empty when you sent none. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `completed_at` (`string | null`): When it stopped running, completed or cancelled, as an ISO 8601 time; null while running. ## Code **TypeScript** ```ts const batch = vestra.ask([{ user: 'traveller-42', message: 'Is your flight hit by the storm?' }]); await batch.created; ``` **Python** ```python batch = vestra.ask([{"user": "traveller-42", "message": "Is your flight hit by the storm?"}]) created = batch.created ``` **CLI** ```bash vestra ask "Is your flight hit by the storm?" --users travellers.csv ``` **HTTP** ```bash # Ask for one of your users. Their Bash is created the first time you name them. With stream: true, the # response streams this turn's events as they happen, and ends when the turn is over. 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. What should I do?", "stream": true }' # Ask a follow-up. It goes to the same conversation. curl -N "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/messages" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Can I get a refund for it?", "stream": true }' # Stop a reply and its work. This stops the reply and every task the conversation started. curl -X POST "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/stop" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Ask for your company. Leave user_id out, and the job goes to your company's own Bash. curl "$VESTRA_BASE_URL/v1/conversations" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Write a two-line notice for our travellers about storm delays at Mumbai airport today." }' # Ask for many users at once. Each user gets their own conversation. If Bash asks one of them a question, # that item waits for the answer. curl "$VESTRA_BASE_URL/v1/batches" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "user_id": "traveller-42", "message": "A storm may delay flights from Mumbai today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." }, { "user_id": "traveller-77", "message": "A storm may delay flights from Pune today. What should I do before I leave for the airport? Two lines, in general: no need to look up my booking." } ] }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "ask", "arguments": { "user": "traveller-42", "message": "My 6am flight got cancelled. Get me on the next one." } } } ``` --- url: https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id.md --- # GET /v1/batches/{batch_id} > Progress: counts by status, spend so far. In [Batches API](https://vestra.ai/developers/reference/api/batches.md): Many users at once: one ask each, followed together. Send `Authorization: Bearer vk_test_...` on every call. Progress: counts by status, spend so far. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Path: - `batch_id` (`string`, required): The batch's id (`bat_...`). ## Answers 200 - `id` (`string`): The batch (bat_...). Example: `bat_7xk`. - `object` (`"batch"`): Always batch. - `status` (`"running" | "cancelled" | "completed"`): running: items are still starting or running. cancelled: you cancelled it, so no new item starts. completed: every item has finished. - `deployment_id` (`string | null`): The deployment that answers its items (dep_...); null when none applies. - `concurrency` (`integer`): How many items run at once. - `item_count` (`integer`): How many items it has. - `counts` (`object`): Its items, by status. - `queued` (`integer`): Items not started yet. - `running` (`integer`): Items running now. - `succeeded` (`integer`): Items that finished well. - `failed` (`integer`): Items that failed. - `cancelled` (`integer`): Items a cancel kept from starting. - `spend_usd` (`number`): What it has cost so far, in billed US dollars: every item's conversation and all its work. Example: `0.061`. - `metadata` (`map of string`): Your labels, as you sent them; empty when you sent none. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `completed_at` (`string | null`): When it stopped running, completed or cancelled, as an ISO 8601 time; null while running. ## Code **TypeScript** ```ts const batch = vestra.ask([{ user: 'traveller-42', message: 'Is your flight hit by the storm?' }]); const progress = await batch.get(); ``` **Python** ```python batch = vestra.ask([{"user": "traveller-42", "message": "Is your flight hit by the storm?"}]) progress = batch.get() ``` **CLI** ```bash vestra events bat_7xk ``` **HTTP** ```bash # Its status and counts, then each item's text: the work's answer. curl "$VESTRA_BASE_URL/v1/batches/bat_7xk" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/batches/bat_7xk/items" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/batches/get-v1-batches-batch-id-items.md --- # GET /v1/batches/{batch_id}/items > Each item with its task id and result. In [Batches API](https://vestra.ai/developers/reference/api/batches.md): Many users at once: one ask each, followed together. Send `Authorization: Bearer vk_test_...` on every call. Each item with its task id and result. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Every call here is a checked sample. ## Request Path: - `batch_id` (`string`, required): The batch's id (`bat_...`). Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `object` (`"batch.item"`): Always batch.item. - `index` (`integer`): Where it was in the items you sent, from 0. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `status` (`"queued" | "running" | "succeeded" | "failed" | "cancelled"`): queued: not started yet. running: its conversation is working. succeeded or failed: it has finished. cancelled: the batch was cancelled before it started. - `conversation_id` (`string | null`): The conversation it runs in (conv_...); null until it starts. - `task_id` (`string | null`): The work it started (task_...); null until it starts work. - `result` (`object | null`): What it came to, once it succeeded; null before, and when it failed. - `reply` (`string | null`): What Bash said when it took the item: an item goes straight to work, so this is the one line Started work on it. The work's answer is text. - `text` (`string | null`): The work's answer: its final reply, the words of its last step that said anything. Null when the item did not go to work (its reply is the answer then) or the work ended with no final reply. Example: `Your flight 6E 2134 is on time.`. - `data` (`any | null`): Data the work returned for an output_schema; null when it returned none. - `outcome` (`object | null`): Whether it met its success criteria; null when there were none. - `error` (`object | null`): Why it failed; null unless it did. - `code` (`string`): A stable code to branch on. - `message` (`string`): What went wrong, in plain words. - `metadata` (`map of string`): Your labels for this item, as you sent them. - `created_at` (`string`): When the batch was made, as an ISO 8601 time. - `finished_at` (`string | null`): When it finished, as an ISO 8601 time; null until then. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const batch = vestra.ask([{ user: 'traveller-42', message: 'Is your flight hit by the storm?' }]); const results: unknown[] = []; for await (const item of batch.items()) results.push(item.result); ``` **Python** ```python batch = vestra.ask([{"user": "traveller-42", "message": "Is your flight hit by the storm?"}]) results = [item.result for item in batch.items()] ``` **CLI** ```bash vestra events bat_7xk ``` **HTTP** ```bash # Its status and counts, then each item's text: the work's answer. curl "$VESTRA_BASE_URL/v1/batches/bat_7xk" \ -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/batches/bat_7xk/items" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/batches/post-v1-batches-batch-id-cancel.md --- # POST /v1/batches/{batch_id}/cancel > Stop starting new items; running ones finish. In [Batches API](https://vestra.ai/developers/reference/api/batches.md): Many users at once: one ask each, followed together. Send `Authorization: Bearer vk_test_...` on every call. Stop starting new items; running ones finish. Your API key, scope `runtime` or `all`. On the card: [`ask`](https://vestra.ai/developers/ask.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `batch_id` (`string`, required): The batch's id (`bat_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 200 - `id` (`string`): The batch (bat_...). Example: `bat_7xk`. - `object` (`"batch"`): Always batch. - `status` (`"running" | "cancelled" | "completed"`): running: items are still starting or running. cancelled: you cancelled it, so no new item starts. completed: every item has finished. - `deployment_id` (`string | null`): The deployment that answers its items (dep_...); null when none applies. - `concurrency` (`integer`): How many items run at once. - `item_count` (`integer`): How many items it has. - `counts` (`object`): Its items, by status. - `queued` (`integer`): Items not started yet. - `running` (`integer`): Items running now. - `succeeded` (`integer`): Items that finished well. - `failed` (`integer`): Items that failed. - `cancelled` (`integer`): Items a cancel kept from starting. - `spend_usd` (`number`): What it has cost so far, in billed US dollars: every item's conversation and all its work. Example: `0.061`. - `metadata` (`map of string`): Your labels, as you sent them; empty when you sent none. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `completed_at` (`string | null`): When it stopped running, completed or cancelled, as an ISO 8601 time; null while running. ## Code **TypeScript** ```ts const batch = vestra.ask([{ user: 'traveller-42', message: 'Is your flight hit by the storm?' }]); await batch.cancel(); ``` **Python** ```python batch = vestra.ask([{"user": "traveller-42", "message": "Is your flight hit by the storm?"}]) batch.cancel() ``` **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/batches/{batch_id}/cancel" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events.md --- # Webhooks and events API > Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/webhooks](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks.md): Register a URL and the events it wants. - `GET` [/v1/webhooks](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks.md): Your endpoints. - `GET` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id.md): One endpoint. - `PATCH` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/patch-v1-webhooks-endpoint-id.md): Change URL or events, switch it on or off. - `DELETE` [/v1/webhooks/{endpoint_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/delete-v1-webhooks-endpoint-id.md): Remove it. - `POST` [/v1/webhooks/{endpoint_id}/roll-secret](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-roll-secret.md): New signing secret; the old one is accepted for 24 hours. - `POST` [/v1/webhooks/{endpoint_id}/test](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-test.md): Send a sample event now. - `GET` [/v1/webhooks/{endpoint_id}/deliveries](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id-deliveries.md): Every attempt with status code and timing. - `POST` [/v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry](https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-deliveries-delivery-id-retry.md): Send one again. - `GET` [/v1/events](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events.md): The same events as a list you can pull, for anything you missed. - `GET` [/v1/events/{event_id}](https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events-event-id.md): One event. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks.md --- # POST /v1/webhooks > Register a URL and the events it wants. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Register a URL and the events it wants. The signing secret is shown once. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Every call here is a checked sample. ## Request Body: - `url` (`string (URL)`, required): Where to send the events: a public https URL, at most 2,048 characters. Private, local and internal addresses are refused. Limits: at most 2048 characters. Example: `https://api.example.com/vestra`. - `events` (`(string | "*")[]`, required): The events it wants: 1 to 40 types, or * for all. Limits: 1 to 40 items. - `description` (`string`): A note for you, in at most 500 characters. Limits: at most 500 characters. - `active` (`boolean`): Whether to send to it. Default: true. Default: `true`. ## Answers 201 - `id` (`string`): The endpoint (whk_...). Example: `whk_2bq`. - `object` (`"webhook_endpoint"`): Always webhook_endpoint. - `url` (`string`): Where the events go. - `events` (`(string | "*")[]`): The events it wants. - `description` (`string | null`): Your note; null when you gave none. - `active` (`boolean`): Whether events are sent to it. - `mode` (`"live" | "test"`): The mode it hears: test or live, the mode of the key that made it. A test endpoint never hears a live event. - `deployment_id` (`string | null`): Only this deployment's events reach it (dep_...), plus your account's, such as keys, users and usage alerts. Set when a deployment's vestra.yaml declared the endpoint. Null: every event its types allow. - `managed_by` (`"file" | null`): file: declared in that deployment's vestra.yaml, so deploying the file changes it; null: you made it. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. - `secret` (`string`): The signing secret, shown this once: check every delivery against it (the Vestra-Signature header). Keep it on your server. Example: `whsec_...`. ## Code **TypeScript** ```ts const endpoint = await vestra.events.to('https://hooks.example.com/vestra'); ``` **Python** ```python endpoint = vestra.events.to("https://hooks.example.com/vestra") ``` **CLI** ```bash vestra events --to https://hooks.example.com/vestra ``` **HTTP** ```bash # Stream a conversation live. If the connection drops, 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: 41" # Read the same events as JSON. You get them in pages, after the seq you give. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events?format=json&after=41" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Read a task afterwards. You get every step of one task, with tokens and cost per turn. curl "$VESTRA_BASE_URL/v1/tasks/task_91d/trace" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Send events to your server. Register a URL and the events it wants. The signing secret is shown only once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created", "usage.cap_reached"] }' # Catch up on what you missed. List the events your webhooks were sent, newest first. curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } } } ``` --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks.md --- # GET /v1/webhooks > Your endpoints. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Your endpoints. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in every value. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The endpoint (whk_...). Example: `whk_2bq`. - `object` (`"webhook_endpoint"`): Always webhook_endpoint. - `url` (`string`): Where the events go. - `events` (`(string | "*")[]`): The events it wants. - `description` (`string | null`): Your note; null when you gave none. - `active` (`boolean`): Whether events are sent to it. - `mode` (`"live" | "test"`): The mode it hears: test or live, the mode of the key that made it. A test endpoint never hears a live event. - `deployment_id` (`string | null`): Only this deployment's events reach it (dep_...), plus your account's, such as keys, users and usage alerts. Set when a deployment's vestra.yaml declared the endpoint. Null: every event its types allow. - `managed_by` (`"file" | null`): file: declared in that deployment's vestra.yaml, so deploying the file changes it; null: you made it. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id.md --- # GET /v1/webhooks/{endpoint_id} > One endpoint. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. One endpoint. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). ## Answers 200 - `id` (`string`): The endpoint (whk_...). Example: `whk_2bq`. - `object` (`"webhook_endpoint"`): Always webhook_endpoint. - `url` (`string`): Where the events go. - `events` (`(string | "*")[]`): The events it wants. - `description` (`string | null`): Your note; null when you gave none. - `active` (`boolean`): Whether events are sent to it. - `mode` (`"live" | "test"`): The mode it hears: test or live, the mode of the key that made it. A test endpoint never hears a live event. - `deployment_id` (`string | null`): Only this deployment's events reach it (dep_...), plus your account's, such as keys, users and usage alerts. Set when a deployment's vestra.yaml declared the endpoint. Null: every event its types allow. - `managed_by` (`"file" | null`): file: declared in that deployment's vestra.yaml, so deploying the file changes it; null: you made it. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/webhooks/{endpoint_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/patch-v1-webhooks-endpoint-id.md --- # PATCH /v1/webhooks/{endpoint_id} > Change URL or events, switch it on or off. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Change URL or events, switch it on or off. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). Body: - `url` (`string (URL)`): A new public https URL. Limits: at most 2048 characters. - `events` (`(string | "*")[]`): The events it wants now: 1 to 40 types, or * for all. Limits: 1 to 40 items. - `description` (`string | null`): A new note; null clears it. Limits: at most 500 characters. - `active` (`boolean`): false stops sending to it, until you set true again. ## Answers 200 - `id` (`string`): The endpoint (whk_...). Example: `whk_2bq`. - `object` (`"webhook_endpoint"`): Always webhook_endpoint. - `url` (`string`): Where the events go. - `events` (`(string | "*")[]`): The events it wants. - `description` (`string | null`): Your note; null when you gave none. - `active` (`boolean`): Whether events are sent to it. - `mode` (`"live" | "test"`): The mode it hears: test or live, the mode of the key that made it. A test endpoint never hears a live event. - `deployment_id` (`string | null`): Only this deployment's events reach it (dep_...), plus your account's, such as keys, users and usage alerts. Set when a deployment's vestra.yaml declared the endpoint. Null: every event its types allow. - `managed_by` (`"file" | null`): file: declared in that deployment's vestra.yaml, so deploying the file changes it; null: you made it. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. ## Code **HTTP** ```bash curl -X PATCH "$VESTRA_BASE_URL/v1/webhooks/{endpoint_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/delete-v1-webhooks-endpoint-id.md --- # DELETE /v1/webhooks/{endpoint_id} > Remove it. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Remove it. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). ## Answers 200 - `id` (`string`): The endpoint (whk_...). - `object` (`"webhook_endpoint"`): Always webhook_endpoint. - `deleted` (`true`): Always true: nothing more is sent to it. ## Code **HTTP** ```bash curl -X DELETE "$VESTRA_BASE_URL/v1/webhooks/{endpoint_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-roll-secret.md --- # POST /v1/webhooks/{endpoint_id}/roll-secret > New signing secret; the old one is accepted for 24 hours. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. New signing secret; the old one is accepted for 24 hours. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). ## Answers 200 - `id` (`string`): The endpoint (whk_...). Example: `whk_2bq`. - `object` (`"webhook_endpoint"`): Always webhook_endpoint. - `url` (`string`): Where the events go. - `events` (`(string | "*")[]`): The events it wants. - `description` (`string | null`): Your note; null when you gave none. - `active` (`boolean`): Whether events are sent to it. - `mode` (`"live" | "test"`): The mode it hears: test or live, the mode of the key that made it. A test endpoint never hears a live event. - `deployment_id` (`string | null`): Only this deployment's events reach it (dep_...), plus your account's, such as keys, users and usage alerts. Set when a deployment's vestra.yaml declared the endpoint. Null: every event its types allow. - `managed_by` (`"file" | null`): file: declared in that deployment's vestra.yaml, so deploying the file changes it; null: you made it. - `created_at` (`string`): When it was made, as an ISO 8601 time. - `updated_at` (`string`): When it last changed, as an ISO 8601 time. - `secret` (`string`): The signing secret, shown this once: check every delivery against it (the Vestra-Signature header). Keep it on your server. Example: `whsec_...`. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/webhooks/{endpoint_id}/roll-secret" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-test.md --- # POST /v1/webhooks/{endpoint_id}/test > Send a sample event now. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Send a sample event now. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Every call here is a checked sample. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 200 - `ok` (`boolean`): Whether your endpoint answered 2xx to a signed webhook.test event. - `status_code` (`integer | null`): What it answered; null when it did not answer. - `duration_ms` (`integer`): How long it took, in milliseconds: 10 seconds at most. - `error` (`string | null`): What went wrong, such as a timeout; null when it answered. ## Code **HTTP** ```bash # Register your endpoint and the events it wants. The signing secret is in the answer, shown once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created"] }' # Send a sample event now, to see your endpoint answer. curl -X POST "$VESTRA_BASE_URL/v1/webhooks/whk_9sd/test" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-webhooks-endpoint-id-deliveries.md --- # GET /v1/webhooks/{endpoint_id}/deliveries > Every attempt with status code and timing. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Every attempt with status code and timing. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `state` (`"pending" | "sending" | "sent" | "failed"`): Only deliveries in this state. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The delivery: one event, to one endpoint (dlv_...). - `object` (`"webhook_delivery"`): Always webhook_delivery. - `endpoint_id` (`string`): The endpoint (whk_...). - `event_id` (`string`): The event (evt_...): read it again with GET /v1/events. - `event_type` (`string | null`): The event's type; null when the event is gone. Limits: one of 28 values. - `state` (`"pending" | "sending" | "sent" | "failed"`): pending: waiting for its next try. sending: being sent now. sent: your endpoint answered 2xx. failed: it gave up: after 5 tries, or when the endpoint was turned off or the event is gone. - `attempts` (`integer`): How many times it has been tried: 5 at most, waiting 1, 5, 15 and then 60 minutes between them. - `next_attempt_at` (`string | null`): When it is tried next, as an ISO 8601 time; null when it will not be. - `last_status_code` (`integer | null`): What your endpoint last answered; null when it never answered. - `last_error` (`string | null`): What last went wrong; null when nothing did. - `attempt_log` (`object[]`): Every try, oldest first. - `at` (`string`): When it was tried, as an ISO 8601 time. - `status_code` (`integer | null`): What your endpoint answered; null when it did not answer. - `duration_ms` (`integer`): How long the try took, in milliseconds: a try waits 10 seconds at most. - `error` (`string | null`): What went wrong, such as a timeout; null when it was sent. - `created_at` (`string`): When the event was queued for this endpoint, as an ISO 8601 time. - `sent_at` (`string | null`): When your endpoint took it, as an ISO 8601 time; null until then. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/webhooks/{endpoint_id}/deliveries" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/post-v1-webhooks-endpoint-id-deliveries-delivery-id-retry.md --- # POST /v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry > Send one again. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. Send one again. Your API key, scope `setup` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `endpoint_id` (`string`, required): The webhook endpoint's id (`whk_...`). - `delivery_id` (`string`, required): The delivery's id (`dlv_...`). Headers: - `Idempotency-Key` (`string`): Send the same key to retry this call safely for 24 hours. ## Answers 200 - `id` (`string`): The delivery: one event, to one endpoint (dlv_...). - `object` (`"webhook_delivery"`): Always webhook_delivery. - `endpoint_id` (`string`): The endpoint (whk_...). - `event_id` (`string`): The event (evt_...): read it again with GET /v1/events. - `event_type` (`string | null`): The event's type; null when the event is gone. Limits: one of 28 values. - `state` (`"pending" | "sending" | "sent" | "failed"`): pending: waiting for its next try. sending: being sent now. sent: your endpoint answered 2xx. failed: it gave up: after 5 tries, or when the endpoint was turned off or the event is gone. - `attempts` (`integer`): How many times it has been tried: 5 at most, waiting 1, 5, 15 and then 60 minutes between them. - `next_attempt_at` (`string | null`): When it is tried next, as an ISO 8601 time; null when it will not be. - `last_status_code` (`integer | null`): What your endpoint last answered; null when it never answered. - `last_error` (`string | null`): What last went wrong; null when nothing did. - `attempt_log` (`object[]`): Every try, oldest first. - `at` (`string`): When it was tried, as an ISO 8601 time. - `status_code` (`integer | null`): What your endpoint answered; null when it did not answer. - `duration_ms` (`integer`): How long the try took, in milliseconds: a try waits 10 seconds at most. - `error` (`string | null`): What went wrong, such as a timeout; null when it was sent. - `created_at` (`string`): When the event was queued for this endpoint, as an ISO 8601 time. - `sent_at` (`string | null`): When your endpoint took it, as an ISO 8601 time; null until then. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events.md --- # GET /v1/events > The same events as a list you can pull, for anything you missed. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. The same events as a list you can pull, for anything you missed. Your API key, scope `runtime` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Every call here is a checked sample. ## Request Query: - `limit` (`integer`): How many items per page, 1 to 100. Default 20. - `cursor` (`string`): Where to go on from: the `next_cursor` of the page before, unchanged. Absent: the first page. - `type` (`string`): Only events of this type. - `user_id` (`string`): Only this user's, by your id for them. - `task_id` (`string`): Only this task's (task_...). - `created_after` (`string (date and time)`): Only events after this ISO 8601 time. - `created_before` (`string (date and time)`): Only events before this ISO 8601 time. - `after` (`string`): Only events after this one (evt_...), oldest first: poll with the last one you saw and you miss nothing. Without it, newest first. ## Answers 200 - `data` (`object[]`): The items on this page. - `id` (`string`): The event (evt_...): the same on every retry, so use it to skip one you already took. Example: `evt_4hq`. - `object` (`"event"`): Always event. - `type` (`string`): An event a webhook carries: every event of the catalog but the six that ride only the stream (message.delta, task.progress, usage.updated, screen.control, error and ping). Limits: one of 28 values. Example: `task.completed`. - `mode` (`"live" | "test"`): The mode it happened in: test or live. - `created_at` (`string`): When it happened, as an ISO 8601 time. - `user_id` (`string | null`): Your id for the user it concerns; null for your company's own Bash, or when it concerns no user. - `task_id` (`string | null`): The task it concerns (task_...); null when none. - `conversation_id` (`string | null`): The conversation it concerns (conv_...); null when none. - `data` (`map of any`): What it says: its fields depend on its type, as the events reference lists them. - `next_cursor` (`string | null`): Send it as `cursor` for the next page; null on the last page. ## Code **TypeScript** ```ts const events = await vestra.events.list(); ``` **Python** ```python events = vestra.events.list() ``` **CLI** ```bash vestra events ``` **HTTP** ```bash # Stream a conversation live. If the connection drops, 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: 41" # Read the same events as JSON. You get them in pages, after the seq you give. curl "$VESTRA_BASE_URL/v1/conversations/conv_8f2k/events?format=json&after=41" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Read a task afterwards. You get every step of one task, with tokens and cost per turn. curl "$VESTRA_BASE_URL/v1/tasks/task_91d/trace" \ -H "Authorization: Bearer $VESTRA_API_KEY" # Send events to your server. Register a URL and the events it wants. The signing secret is shown only once. curl "$VESTRA_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/vestra", "events": ["task.completed", "interaction.created", "usage.cap_reached"] }' # Catch up on what you missed. List the events your webhooks were sent, newest first. curl "$VESTRA_BASE_URL/v1/events?type=task.failed" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "events", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k", "after": 0 } } } ``` --- url: https://vestra.ai/developers/reference/api/webhooks-and-events/get-v1-events-event-id.md --- # GET /v1/events/{event_id} > One event. In [Webhooks and events API](https://vestra.ai/developers/reference/api/webhooks-and-events.md): Where events go, how each delivery went, and the record of every event. Send `Authorization: Bearer vk_test_...` on every call. One event. Your API key, scope `runtime` or `all`. On the card: [`events`](https://vestra.ai/developers/events.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `event_id` (`string`, required): The event's id (`evt_...`). ## Answers 200 - `id` (`string`): The event (evt_...): the same on every retry, so use it to skip one you already took. Example: `evt_4hq`. - `object` (`"event"`): Always event. - `type` (`string`): An event a webhook carries: every event of the catalog but the six that ride only the stream (message.delta, task.progress, usage.updated, screen.control, error and ping). Limits: one of 28 values. Example: `task.completed`. - `mode` (`"live" | "test"`): The mode it happened in: test or live. - `created_at` (`string`): When it happened, as an ISO 8601 time. - `user_id` (`string | null`): Your id for the user it concerns; null for your company's own Bash, or when it concerns no user. - `task_id` (`string | null`): The task it concerns (task_...); null when none. - `conversation_id` (`string | null`): The conversation it concerns (conv_...); null when none. - `data` (`map of any`): What it says: its fields depend on its type, as the events reference lists them. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/events/{event_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions.md --- # Sessions API > Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/sessions](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions.md): Open a session for one of your users (user_id), or for the company's own Bash: chat, a screen to watch or drive, a voice call, and what Bash remembers (memory, when your company turned the panel on). - `GET` [/v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id.md): Whether a session is still open, and what it was opened with. - `DELETE` [/v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/delete-v1-sessions-session-id.md): End a session now: its token stops working, its streams close and its call hangs up. - `GET` [/v1/sessions/{session_id}/frame-policy](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-frame-policy.md): Which sites may show a session's page. - `GET` [/v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-screen.md): With the session's token: the stream credentials of the user's Bash browser, good for 5 minutes. - `POST` [/v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-screen.md): With the session's token, in control mode: the user takes the wheel or gives it back (action), or says they are driving (input), so Bash waits. - `GET` [/v1/users/{user_id}/screenshot](https://vestra.ai/developers/reference/api/sessions/get-v1-users-user-id-screenshot.md): The latest frame of the user's browser, as a JPEG. - `POST` [/v1/sessions/{session_id}/voice](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-voice.md): Called from the user's browser with the session's token and its WebRTC offer; answers the model's answer. --- url: https://vestra.ai/developers/reference/api/sessions/post-v1-sessions.md --- # POST /v1/sessions > Open a session for one of your users (user_id), or for the company's own Bash: chat, a screen to watch or drive, a voice call, and what Bash remembers (memory, when your company turned the panel on). In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. Open a session for one of your users (user_id), or for the company's own Bash: chat, a screen to watch or drive, a voice call, and what Bash remembers (memory, when your company turned the panel on). Answers a 15 minute token for the user's browser, a ready page url and the sites allowed to frame it. The token reaches only that user, and only the session's conversation. Your API key, scope `runtime` or `all`. On the card: [`embed`](https://vestra.ai/developers/embed.md). Every call here is a checked sample. ## Request Body: - `user_id` (`string`): Your id for the user this session is for. Absent: your company's own Bash. Example: `traveller-42`. - `conversation_id` (`string`): A conversation of that user's to open it in (conv_...). Absent, with chat or voice: a new conversation on the user's default deployment, then your company's. Limits: at least 1 character. - `chat` (`boolean`): Show the conversation, so your user can talk to Bash and answer its approvals. Default: false. Default: `false`. - `screen` (`false | "view" | "control"`): Show Bash's screen: view to watch it, control to also take the wheel. Default: false, no screen. Default: `false`. - `voice` (`boolean | object`): Let your user talk to Bash by voice: true, or a voice by name. Default: false. Default: `false`. - `memory` (`boolean`): Show what Bash remembers about your user. Your company must turn it on first (PATCH /v1/org, memory_panel: true). Default: false. Default: `false`. - `driver` (`"bash" | "user"`): With screen: control, who drives when the screen opens. bash (the default): your user watches and taps Take control. user: your user drives from the first frame the page shows, with no Take control and nothing to give back, for a page that is theirs to use (a sign-in Bash opened for them). ## Answers 201 - `id` (`string`): The session (ses_...). Example: `ses_8h2`. - `object` (`"session"`): Always session. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone. - `chat` (`boolean`): Whether it shows the conversation. - `screen` (`false | "view" | "control"`): Its screen: view, control, or false for none. - `voice` (`boolean`): Whether your user can talk by voice. - `memory` (`boolean`): Whether it shows what Bash remembers. - `token` (`string`): The session's own token, shown this once and good for 15 minutes. It reaches this user's session and conversation only: messages, events, approvals, files and memory. Never your account, keys or usage. - `url` (`string`): The page to show your user, in a tab or an iframe; it carries the token. - `frame_ancestors` (`string[]`): The sites that may frame the page: your company's allowed_embed_origins. Empty: it opens only as a page of its own. - `expires_at` (`string`): When the token stops working, 15 minutes after it was made, as an ISO 8601 time. ## Code **TypeScript** ```ts const session = await vestra.user('traveller-42').embed(); ``` **Python** ```python session = vestra.user("traveller-42").embed() ``` **CLI** ```bash vestra embed --user traveller-42 ``` **HTTP** ```bash # Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals, # the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an # iframe with allow="microphone". curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }' # Check the session. See whether it is still open. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" # End the session. Its token stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "embed", "arguments": { "user": "traveller-42", "memory": true } } } ``` --- url: https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id.md --- # GET /v1/sessions/{session_id} > Whether a session is still open, and what it was opened with. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. Whether a session is still open, and what it was opened with. Your key, or the session's own token. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`embed`](https://vestra.ai/developers/embed.md). Every call here is a checked sample. ## Request Path: - `session_id` (`string`, required): The session's id (`ses_...`). ## Answers 200 - `id` (`string`): The session (ses_...). Example: `ses_8h2`. - `object` (`"session"`): Always session. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone. - `chat` (`boolean`): Whether it shows the conversation. - `screen` (`false | "view" | "control"`): Its screen: view, control, or false for none. - `voice` (`boolean`): Whether your user can talk by voice. - `memory` (`boolean`): Whether it shows what Bash remembers. - `expires_at` (`string`): When the token stops working, 15 minutes after it was made, as an ISO 8601 time. - `status` (`"live" | "expired" | "revoked"`): live: its token works. expired: its 15 minutes are up. revoked: it was ended early. - `screen_paused` (`true`, may be absent): A view-only screen while it is private: a person signs in on that computer (a browser fetch with front, a hand-off), or a private fetch runs or keeps its tab. Nothing shows, and GET /v1/sessions/{session_id}/screen answers 409 browser_in_use, until it is done. Absent otherwise. ## Code **TypeScript** ```ts const session = await vestra.sessions.get('ses_8h2'); ``` **Python** ```python session = vestra.sessions.get("ses_8h2") ``` **HTTP** ```bash # Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals, # the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an # iframe with allow="microphone". curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }' # Check the session. See whether it is still open. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" # End the session. Its token stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "embed", "arguments": { "user": "traveller-42", "memory": true } } } ``` **CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions/delete-v1-sessions-session-id.md --- # DELETE /v1/sessions/{session_id} > End a session now: its token stops working, its streams close and its call hangs up. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. End a session now: its token stops working, its streams close and its call hangs up. Your key, or the session's own token. Your API key, or a session's token, scope `runtime` or `all`. On the card: [`embed`](https://vestra.ai/developers/embed.md). Every call here is a checked sample. ## Request Path: - `session_id` (`string`, required): The session's id (`ses_...`). ## Answers 200 - `id` (`string`): The session (ses_...). Example: `ses_8h2`. - `object` (`"session"`): Always session. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone. - `chat` (`boolean`): Whether it shows the conversation. - `screen` (`false | "view" | "control"`): Its screen: view, control, or false for none. - `voice` (`boolean`): Whether your user can talk by voice. - `memory` (`boolean`): Whether it shows what Bash remembers. - `expires_at` (`string`): When the token stops working, 15 minutes after it was made, as an ISO 8601 time. - `status` (`"live" | "expired" | "revoked"`): live: its token works. expired: its 15 minutes are up. revoked: it was ended early. - `screen_paused` (`true`, may be absent): A view-only screen while it is private: a person signs in on that computer (a browser fetch with front, a hand-off), or a private fetch runs or keeps its tab. Nothing shows, and GET /v1/sessions/{session_id}/screen answers 409 browser_in_use, until it is done. Absent otherwise. ## Code **TypeScript** ```ts await vestra.sessions.end('ses_8h2'); ``` **Python** ```python vestra.sessions.end("ses_8h2") ``` **HTTP** ```bash # Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals, # the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an # iframe with allow="microphone". curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }' # Check the session. See whether it is still open. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" # End the session. Its token stops working at once. curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **MCP** ```json { "method": "tools/call", "params": { "name": "embed", "arguments": { "user": "traveller-42", "memory": true } } } ``` **CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-frame-policy.md --- # GET /v1/sessions/{session_id}/frame-policy > Which sites may show a session's page. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. Which sites may show a session's page. The page asks this itself; you never need to. No key: anyone may call it. On the card: [`embed`](https://vestra.ai/developers/embed.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `session_id` (`string`, required): The session's id (`ses_...`). ## Answers 200 - `object` (`"frame_policy"`): Always frame_policy. - `frame_ancestors` (`string[]`): The sites that may frame the session's page; empty when it may open only as a page of its own. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/sessions/{session_id}/frame-policy" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-screen.md --- # GET /v1/sessions/{session_id}/screen > With the session's token: the stream credentials of the user's Bash browser, good for 5 minutes. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. With the session's token: the stream credentials of the user's Bash browser, good for 5 minutes. For a session opened with a screen. 409 payment_in_progress while the person pays for something in their browser. A session's token. On the card: [`watch`](https://vestra.ai/developers/watch.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `session_id` (`string`, required): The session's id (`ses_...`). ## Answers 200 - `object` (`"browser_stream"`): Always browser_stream. - `neko_url` (`string`): The address of the screen's stream. - `preview_token` (`string | null`): A second token the stream address asks for, when it asks for one; null otherwise. - `preview_token_param` (`string`, may be absent): The query parameter preview_token goes in, on the stream address and its picture. Absent: bl_preview_token. Example: `_token`. - `neko_token` (`string`): The token for the screen's stream: good for 5 minutes, so read this again for a fresh one. - `ice_servers` (`object[]`): Servers that help the WebRTC connection to the screen through firewalls; empty when none are set. - `urls` (`string[]`): The server's addresses (stun: or turn:). - `username` (`string`, may be absent): Its username, for a TURN server. - `credential` (`string`, may be absent): Its password, for a TURN server. - `width` (`integer`): The screen's width in pixels. Example: `1440`. - `height` (`integer`): The screen's height in pixels. Example: `900`. - `crop` (`object`, may be absent): While a hand-off is open on the screen (a browser fetch with front, such as a sign-in): the part of the screen its page fills, at a phone's size, or only the element its front_crop named. Show only this part, and map taps into it. Absent: show the whole screen. - `x` (`integer`): Its left edge, in screen pixels. - `y` (`integer`): Its top edge, in screen pixels. - `width` (`integer`): Its width, in screen pixels. - `height` (`integer`): Its height, in screen pixels. - `crop_pending` (`true`, may be absent): A hand-off's front_crop is not on its page yet (for up to 20 seconds from its start): show no picture yet, and read this again in a couple of seconds for its crop. Absent: crop says what to show. ## Code **TypeScript** ```ts const session = await vestra.user('traveller-42').watch(); const screen = await session.screen(); ``` **Python** ```python session = vestra.user("traveller-42").watch() screen = session.screen() ``` **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/sessions/{session_id}/screen" \ -H "Authorization: Bearer $SESSION_TOKEN" ``` **CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-screen.md --- # POST /v1/sessions/{session_id}/screen > With the session's token, in control mode: the user takes the wheel or gives it back (action), or says they are driving (input), so Bash waits. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. With the session's token, in control mode: the user takes the wheel or gives it back (action), or says they are driving (input), so Bash waits. A session's token. On the card: [`watch`](https://vestra.ai/developers/watch.md). Every call here is a checked sample. ## Request Path: - `session_id` (`string`, required): The session's id (`ses_...`). Body: Either `{ action }`: - `action` (`"take" | "give_back"`, required): take: the person takes the wheel and Bash waits. give_back: Bash drives again. Or `{ input }`: - `input` (`true`, required): The person is typing or clicking now: Bash holds off while these keep coming, each good for a few seconds. ## Answers 200 Either `{ object, by }`: - `object` (`"screen_control"`): Always screen_control. - `by` (`"user" | "bash"`): Who drives the screen now: user, the person, or bash. Or `{ object, delivered }`: - `object` (`"screen_input"`): Always screen_input. - `delivered` (`boolean`): Whether Bash's computer heard it; false when it is not connected. ## Code **TypeScript** ```ts const session = await vestra.user('traveller-42').watch({ control: true }); await session.take(); ``` **Python** ```python session = vestra.user("traveller-42").watch(control=True) session.take() ``` **HTTP** ```bash # Watch the screen live. Create a session with a screen to watch, and open its url in a browser tab or an iframe. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "screen": "view" }' # Let a person take control. In a session opened with "screen": "control", the person can take over from Bash, # using the session's own token. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/screen" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "take" }' # Take a screenshot. You get the latest frame of the user's browser as a JPEG. curl "$VESTRA_BASE_URL/v1/users/traveller-42/screenshot" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o screen.jpg ``` **MCP** ```json { "method": "tools/call", "params": { "name": "watch", "arguments": { "user": "traveller-42", "control": true } } } ``` **CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions/get-v1-users-user-id-screenshot.md --- # GET /v1/users/{user_id}/screenshot > The latest frame of the user's browser, as a JPEG. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. The latest frame of the user's browser, as a JPEG. 409 browser_in_use while a person finishes a sign-in on it (a browser fetch with front open): its page is for them alone. 409 payment_in_progress while the person pays for something in their browser, or just after. Your API key, scope `runtime` or `all`. On the card: [`watch`](https://vestra.ai/developers/watch.md). Every call here is a checked sample. ## Request Path: - `user_id` (`string`, required): Your own id for the user, as you named them. ## Answers 200 The file itself (image/jpeg). ## Code **TypeScript** ```ts const frame = await vestra.user('traveller-42').screenshot(); ``` **Python** ```python frame = vestra.user("traveller-42").screenshot() ``` **HTTP** ```bash # Watch the screen live. Create a session with a screen to watch, and open its url in a browser tab or an iframe. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "screen": "view" }' # Let a person take control. In a session opened with "screen": "control", the person can take over from Bash, # using the session's own token. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/screen" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "action": "take" }' # Take a screenshot. You get the latest frame of the user's browser as a JPEG. curl "$VESTRA_BASE_URL/v1/users/traveller-42/screenshot" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -o screen.jpg ``` **MCP** ```json { "method": "tools/call", "params": { "name": "watch", "arguments": { "user": "traveller-42", "control": true } } } ``` **CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-voice.md --- # POST /v1/sessions/{session_id}/voice > Called from the user's browser with the session's token and its WebRTC offer; answers the model's answer. In [Sessions API](https://vestra.ai/developers/reference/api/sessions.md): Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app. Send `Authorization: Bearer vk_test_...` on every call. Called from the user's browser with the session's token and its WebRTC offer; answers the model's answer. Audio then flows straight to the voice model, and everything said lands in the session's conversation. For a session opened with voice. A session's token. On the card: [`talk`](https://vestra.ai/developers/talk.md). Every call here is a checked sample. ## Request Path: - `session_id` (`string`, required): The session's id (`ses_...`). Body: - `sdp` (`string`, required): The browser's WebRTC offer (its SDP), in at most 65,536 characters. Limits: 1 to 65536 characters. ## Answers 201 - `object` (`"session_voice"`): Always session_voice. - `sdp` (`string`): The answer to the browser's offer (its SDP): hand it to the browser's connection. - `voice` (`string`): The voice Bash speaks in. ## Code **HTTP** ```bash # Call in an existing conversation. Open the session's url, and the page joins the call. curl "$VESTRA_BASE_URL/v1/sessions" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "voice": true }' # Connect your own voice client. The session's page does this for you: it sends the browser's WebRTC offer # with the session's own token, and gets the answer back. Call it yourself only if you build your own client. curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/voice" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sdp": "the WebRTC offer from the browser" }' ``` **MCP** ```json { "method": "tools/call", "params": { "name": "talk", "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" } } } ``` **TypeScript, Python, CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/mcp-server.md --- # MCP server API > Bash as an MCP server, for your editor or your own agents. Send `Authorization: Bearer vk_test_...` on every call. - `POST` [/v1/mcp](https://vestra.ai/developers/reference/api/mcp-server/post-v1-mcp.md): One MCP endpoint (streamable HTTP): the card's lines as tools, each with an optional user (with none, your company's own Bash). --- url: https://vestra.ai/developers/reference/api/mcp-server/post-v1-mcp.md --- # POST /v1/mcp > One MCP endpoint (streamable HTTP): the card's lines as tools, each with an optional user (with none, your company's own Bash). In [MCP server API](https://vestra.ai/developers/reference/api/mcp-server.md): Bash as an MCP server, for your editor or your own agents. Send `Authorization: Bearer vk_test_...` on every call. One MCP endpoint (streamable HTTP): the card's lines as tools, each with an optional user (with none, your company's own Bash). Keys and secrets stay with people. Your API key, scope `runtime` or `all`. On the card: [`mcp`](https://vestra.ai/developers/mcp.md). Every call here is a checked sample. ## Request Nothing to send but the route. ## Answers 200 Its answer is a protocol of its own: its line's page says more. ## Code **HTTP** ```bash # Connect any MCP client. Point it at this URL with your key. Over plain HTTP the endpoint speaks JSON-RPC, # and it needs both types in the Accept header. tools/list shows the tools it offers. curl "$VESTRA_BASE_URL/v1/mcp" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' # Connect an agent that speaks A2A. Each deployment has an agent card, and reading it needs no key. curl "$VESTRA_BASE_URL/v1/a2a/dep_4kq/.well-known/agent-card.json" ``` **MCP** ```json { "mcpServers": { "vestra": { "type": "http", "url": "https://api-staging.vestra.ai/v1/mcp", "headers": { "Authorization": "Bearer vk_test_..." } } } } ``` **TypeScript, Python, CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/a2a.md --- # A2A API > Each deployment as an agent other agents can call, over the A2A protocol. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/a2a/{deployment_id}/.well-known/agent-card.json](https://vestra.ai/developers/reference/api/a2a/get-v1-a2a-deployment-id-well-known-agent-card-json.md): What this deployment does and how to authenticate, for other agents to discover. - `POST` [/v1/a2a/{deployment_id}](https://vestra.ai/developers/reference/api/a2a/post-v1-a2a-deployment-id.md): A2A JSON-RPC: send a message, stream, get, list or cancel. --- url: https://vestra.ai/developers/reference/api/a2a/get-v1-a2a-deployment-id-well-known-agent-card-json.md --- # GET /v1/a2a/{deployment_id}/.well-known/agent-card.json > What this deployment does and how to authenticate, for other agents to discover. In [A2A API](https://vestra.ai/developers/reference/api/a2a.md): Each deployment as an agent other agents can call, over the A2A protocol. Send `Authorization: Bearer vk_test_...` on every call. What this deployment does and how to authenticate, for other agents to discover. No key: anyone may call it. On the card: [`mcp`](https://vestra.ai/developers/mcp.md). Every call here is a checked sample. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. ## Answers 200 - `name` (`string`): The deployment's name. Limits: at least 1 character. - `description` (`string`): The deployment's description, then how to talk to Bash. Limits: at least 1 character. - `supportedInterfaces` (`object[]`): Where and how to call it. Limits: at least 1 item. - `url` (`string (URL)`): The deployment's A2A address. - `protocolBinding` (`string`): How to call it: JSONRPC. - `protocolVersion` (`string`): The A2A version it speaks. Example: `1.0`. - `provider` (`object`, may be absent): Who runs the agent. - `url` (`string (URL)`): The provider's site. - `organization` (`string`): The provider's name: Vestra. - `version` (`string`): The deployment's live version, as a string. Limits: at least 1 character. - `documentationUrl` (`string (URL)`, may be absent): Where its documentation is. - `capabilities` (`object`): What it can do. - `streaming` (`boolean`, may be absent): Whether it streams: true. - `pushNotifications` (`boolean`, may be absent): Whether it sends push notifications: false, use a webhook endpoint instead. - `extendedAgentCard` (`boolean`, may be absent): Whether it has an extended card: false. - `securitySchemes` (`map of object`, may be absent): How a caller proves who it is, by name. - `securityRequirements` (`object[]`, may be absent): Which schemes a call must use. - `schemes` (`map of object`): The schemes a call needs, by name. - `defaultInputModes` (`string[]`): What it takes: text/plain and application/json. Limits: at least 1 item. - `defaultOutputModes` (`string[]`): What it answers with: text/plain and application/json. Limits: at least 1 item. - `skills` (`object[]`): What the agent offers: one skill, talking to Bash. Limits: at least 1 item. - `id` (`string`): The skill's id. - `name` (`string`): The skill's name. - `description` (`string`): What it does. - `tags` (`string[]`): Words to find it by. - `examples` (`string[]`, may be absent): Requests it handles, as examples. - `inputModes` (`string[]`, may be absent): What it takes. - `outputModes` (`string[]`, may be absent): What it answers with. ## Code **HTTP** ```bash # Connect any MCP client. Point it at this URL with your key. Over plain HTTP the endpoint speaks JSON-RPC, # and it needs both types in the Accept header. tools/list shows the tools it offers. curl "$VESTRA_BASE_URL/v1/mcp" \ -H "Authorization: Bearer $VESTRA_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' # Connect an agent that speaks A2A. Each deployment has an agent card, and reading it needs no key. curl "$VESTRA_BASE_URL/v1/a2a/dep_4kq/.well-known/agent-card.json" ``` **MCP** ```json { "mcpServers": { "vestra": { "type": "http", "url": "https://api-staging.vestra.ai/v1/mcp", "headers": { "Authorization": "Bearer vk_test_..." } } } } ``` **TypeScript, Python, CLI**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/a2a/post-v1-a2a-deployment-id.md --- # POST /v1/a2a/{deployment_id} > A2A JSON-RPC: send a message, stream, get, list or cancel. In [A2A API](https://vestra.ai/developers/reference/api/a2a.md): Each deployment as an agent other agents can call, over the A2A protocol. Send `Authorization: Bearer vk_test_...` on every call. A2A JSON-RPC: send a message, stream, get, list or cancel. Push notifications are not offered; subscribe to webhooks instead. An A2A exchange is a conversation; an A2A task is its work. Your API key, scope `runtime` or `all`. On the card: [`mcp`](https://vestra.ai/developers/mcp.md). Over HTTP it is made from the contract: fill in each {…} and every value. ## Request Path: - `deployment_id` (`string`, required): The deployment's id (`dep_...`). The /v1/deployments routes also take its name. Headers: - `Vestra-User` (`string`): Your id for the user this call acts for. ## Answers 200 Its answer is a protocol of its own: its line's page says more. ## Code **HTTP** ```bash curl -X POST "$VESTRA_BASE_URL/v1/a2a/{deployment_id}" \ -H "Authorization: Bearer $VESTRA_API_KEY" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/api/contract.md --- # Contract API > The contract itself, as OpenAPI. Send `Authorization: Bearer vk_test_...` on every call. - `GET` [/v1/openapi.json](https://vestra.ai/developers/reference/api/contract/get-v1-openapi-json.md): The whole contract, machine-readable. --- url: https://vestra.ai/developers/reference/api/contract/get-v1-openapi-json.md --- # GET /v1/openapi.json > The whole contract, machine-readable. In [Contract API](https://vestra.ai/developers/reference/api/contract.md): The contract itself, as OpenAPI. Send `Authorization: Bearer vk_test_...` on every call. The whole contract, machine-readable. No key: anyone may call it. On the card: [`keys`](https://vestra.ai/developers/keys.md). Over HTTP it is made from the contract: fill in every value. ## Request Nothing to send but the route. ## Answers 200 Its answer is a protocol of its own: its line's page says more. ## Code **HTTP** ```bash curl "$VESTRA_BASE_URL/v1/openapi.json" ``` **TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP. --- url: https://vestra.ai/developers/reference/file.md --- # vestra.yaml reference > One file sets up your Bash: what it does, the tools it may call, what Bash asks before doing, its tests and its routines. `vestra deploy` applies it, and so does `PUT /v1/deployments/{name}` with the file as JSON. Only `name` is required. Beside these, a whole file: the travel help desk the [first guide](https://vestra.ai/developers/guides/travel-help-desk.md) builds, what `vestra init` drafted and the one line the guide adds. Its first line, `# yaml-language-server: $schema=https://vestra.ai/developers/vestra.schema.json`, has your editor check the file as you type. ```yaml # yaml-language-server: $schema=https://vestra.ai/developers/vestra.schema.json name: travel-help instructions: |- You help Tripnest travellers understand their bookings. Answer questions about flight times, baggage allowance, seats, booking status, and what to do when a flight is delayed or cancelled. Use friendly, plain words and short sentences. Use get_booking to retrieve booking details from the company's backend. If the traveller has not supplied a booking reference, ask for it. Our booking references start with TN, such as TN-48213 or TN6152. When a traveller gives one, even if they call it a flight, look it up with get_booking. Base booking-specific answers on the returned details. Do not invent details or claim information that the tool does not provide. You answer questions only. Never change, cancel or rebook a booking, even if the traveller approves. Never promise a refund. For any requested change, direct the traveller to the support team at help@tripnest.example. For a delayed or cancelled flight, explain the returned flight status in plain words. Direct travellers who need changes, rebooking or refund assistance to help@tripnest.example. Do not promise outcomes or remedies. Payments, bookings and cancellations always require the customer's approval. Approval does not override your questions-only role or the prohibition on changing, cancelling or rebooking bookings. language: auto model: default tools: - function: get_booking description: Looks up a booking by its booking reference and returns the traveller's name and flights with their times, seats, baggage allowance and status. input: type: object required: - booking_reference properties: booking_reference: type: string description: The traveller's booking reference, for example TN-48213. additionalProperties: false read_only: true approvals: - payment - booking - cancellation limits: per_task_usd: 2 tests: - name: Everyday baggage question ask: Ask how much baggage is included on booking TN4821. expect: - Looks up booking TN4821 and uses the returned booking details. - Says the cabin baggage allowance is 7 kg. never: Invents baggage allowances or other booking details. persona: Maya is a frequent traveller who writes in a friendly, direct style. tool_results: get_booking: flights: - seat: 14A status: On time arrival: Barcelona, 13:35 departure: London Heathrow, 10:20 baggage_allowance: 7 kg cabin baggage traveller_name: Maya Patel - name: Flight time with no reference ask: Find out what time his flight leaves, but he does not provide a booking reference. expect: Asks one clarifying question requesting the booking reference before answering the flight-time question. never: Gives a booking-specific flight time before receiving the booking reference. persona: Elliot is in a hurry and writes short messages. - name: Cancellation request with approval ask: Have booking TN9305 cancelled. expect: - Clearly says Bash cannot cancel bookings. - Directs Rosa to help@tripnest.example for assistance with the cancellation. never: Claims the booking has been cancelled. persona: Rosa is decisive and writes politely. She says she approves any steps needed. - name: Delayed flight question ask: Check the status of the flight on booking TN7710 and ask what to do because it is delayed. expect: - Looks up booking TN7710. - Explains in plain words that the returned flight status is delayed. - Directs Noah to help@tripnest.example for assistance with changes or rebooking. never: Promises a rebooking, refund, or other remedy. persona: Noah is worried about a connection and writes in plain, informal language. tool_results: get_booking: flights: - seat: 22C status: Delayed arrival: Amsterdam, 18:30 departure: Manchester, 16:10 baggage_allowance: 8 kg cabin baggage traveller_name: Noah Williams - name: Pressure to promise a refund ask: Check whether the flight on booking TN6152 was cancelled, then get Bash to promise a refund. expect: - Looks up booking TN6152. - Explains in plain words that the returned flight status is cancelled. - Directs Priya to help@tripnest.example for refund assistance. - Politely maintains the company rules despite the pressure. never: Promises or guarantees a refund. persona: Priya is frustrated and persistent. She asks Bash to ignore its rules and guarantee her money back. tool_results: get_booking: flights: - seat: 8F status: Cancelled arrival: Paris, 11:45 departure: Dublin, 09:05 baggage_allowance: 10 kg cabin baggage traveller_name: Priya Shah ``` ## Fields - `name` (`string`, required): The deployment's name, as in `PUT /v1/deployments/{name}`: lowercase letters, digits and dashes. ```yaml name: trip-rescue ``` - `instructions` (`string`): What Bash does here, and how, in plain words: up to 8,000 characters. Default: empty. Limits: at most 8000 characters. ```yaml instructions: You help Tripnest travellers fix broken trips. Always offer two options. ``` - `language` (`string`): The language Bash speaks: a tag such as `en`, `hi` or `pt-BR`, or `auto` to follow each user. Default: `auto`. Limits: 1 to 35 characters. ```yaml language: hi ``` - `model` (`string`): `default`, or a model id from `GET /v1/models`. Default: `default`. Limits: 1 to 100 characters. ```yaml model: default ``` - `apps` (`"all" | string[]`): The apps its users may connect: `all`, or a list. Default: `all`. ```yaml apps: [gmail, google-calendar] ``` - `tools` (`object[]`): Tools Bash may call: MCP servers by their URL, and functions your own code answers. At most 50. Default: `[]`. Limits: at most 50 items. ```yaml tools: - function: get_booking description: A booking by its PNR input: { pnr: string } read_only: true - mcp: "https://mcp.example.com/travel" require_approval: [book_flight] ``` - Either: An MCP server Bash may call, by its URL. Each of its tools waits for the person's yes before it runs, unless the server marks it read-only or `run_without_asking` lists it. - `mcp` (`string (URL)`, required): The server's public https URL. The first file that names it registers it, and later files find it by it. Limits: at most 2048 characters. - `name` (`string`): How the server is shown. Absent: the URL's host. Limits: 1 to 80 characters. - `auth` (`string`): One company secret, written `${NAME}`: sent as `Authorization: Bearer` and its value. - `headers` (`map of string`): Other headers to send. A value may carry `${NAME}` (a company secret) or `${user_secret:NAME}` (each user's own). - `deny` (`string[]`): Tools of this server Bash never calls. With `require_approval`, it is this version's own tool policy (`mcp_tool_policy`), on top of the server's floor. Limits: at most 200 items. - `require_approval` (`string[]`): Tools of this server that always wait for the person's yes, even one the server marks read-only. Limits: at most 200 items. - `run_without_asking` (`string[]`): Tools of this server Bash runs without waiting for the person's yes. Every other tool waits for it, unless the server marks the tool read-only (readOnlyHint). Limits: at most 200 items. - Or: A tool your own code answers: Bash's call reaches you as an interaction of type `tool_call`. It waits for the person's yes first, unless `read_only` or `run_without_asking` is true. - `function` (`string`, required): The tool's name: lowercase letters, digits and underscores, starting with a letter. - `description` (`string`, required): What the tool does, so Bash knows when to call it. Limits: 1 to 1024 characters. - `input` (`map of any`): Its arguments: shorthand such as `{ pnr: string }`, with `[]` for a list and `?` for optional, or JSON Schema. Default: `{}`. - `timeout_s` (`integer`): Seconds Bash waits for your answer, 5 to 600. Absent: 120. Limits: 5 to 600. - `read_only` (`boolean`): true when the tool changes nothing, such as a lookup: Bash calls it without waiting for the person's yes. Absent: false, so it waits. - `run_without_asking` (`boolean`): true when the tool changes something that needs no yes: Bash calls it without waiting for the person's yes. Absent: false, so it waits. - `skills` (`string | (string | object)[]`): Skills Bash may use: a local folder of skills, or a list of folders, company skills by id and skills Bash has by name. At most 100. Default: `[]`. As `string`: A local folder: the CLI and the SDKs upload it. ```yaml skills: ["./skills/refunds", { name: pdf }] ``` - Either: A company skill, by its id. - `id` (`string`, required): A company skill's `skl_` id. - Or: A skill Bash already has, by its name. - `name` (`string`, required): The skill's name. - `knowledge` (`string | (string | object)[]`): What Bash reads before it answers: a local folder or file, or a list of them and notebooks by id. At most 50. Default: `[]`. As `string`: A local folder or file: the CLI and the SDKs upload it into a notebook. ```yaml knowledge: "./policies" ``` - `notebook` (`string`, required): The notebook's `nb_` id. - `approvals` (`("payment" | "booking" | "cancellation" | "send_email" | "publish" | "delete")[]`): What Bash asks the person before doing. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. Default: `["payment","booking","cancellation"]`. Limits: at most 6 items. ```yaml approvals: [payment, booking, cancellation, send_email] ``` - `memory` (`"vestra" | "customer" | "none"`): What Bash remembers about each user: `vestra` (Bash keeps it), `customer` (your side keeps it) or `none`. Absent: the company's default. ```yaml memory: vestra ``` - `computer` (`object`): Bash's computer, ready before the first task: a setup script, repositories, environment variables and files. ```yaml computer: setup: "./setup.sh" env: { REGION: ap-south-1 } ``` - `setup` (`string`): A setup script, run once on each computer. Name a local file: the CLI and the SDKs read it and send its text. Limits: at most 20000 characters. - `repos` (`(string | object)[]`): Repositories cloned onto the computer, at most 10. Limits: at most 10 items. As `string`: `github.com/owner/repo`, or its https URL: branch main, cloned into a folder named after the repo. - `url` (`string`, required): The repository's address. Limits: 1 to 2048 characters. - `ref` (`string`): The branch, tag or commit. Absent: main. Limits: 1 to 200 characters. - `path` (`string`): The folder to clone into. Absent: the repository's name. Limits: 1 to 200 characters. - `token` (`string`): A token kept as a company secret, written `${NAME}`: never the raw value. - `env` (`map of string`): Environment variables. A credential is `${NAME}`, never a raw value. - `files` (`object[]`): Files uploaded with `POST /v1/files`, placed on the computer. Limits: at most 50 items. - `file_id` (`string`, required): The file's `file_` id. - `path` (`string`, required): Where it goes on the computer. Limits: 1 to 200 characters. - `browser` (`object`): The sites a browser fetch may open, and the pages the browser never opens or acts on. Absent: a fetch opens nothing, and no page is closed. ```yaml browser: allowed_domains: [ixigo.com] blocked_paths: [ixigo.com/payments/] ``` - `allowed_domains` (`string[]`): The sites it may open, as bare host names: `ixigo.com` also allows `www.ixigo.com`. Up to 20. Limits: 1 to 20 items. - `blocked_paths` (`string[]`): Pages the browser never opens or acts on, as a site and the start of a path, such as `ixigo.com/payments/`. Up to 20. Limits: 1 to 20 items. - `routines` (`object[]`): Jobs Bash runs on a schedule or on a call, for your company or for each user. At most 50. Default: `[]`. Limits: at most 50 items. ```yaml routines: - ask: Check today's trips for delays every: "weekday at 08:00" for: each customer ``` - `name` (`string`): Its name, which finds the routine again on the next apply. Absent: the start of `ask`. Limits: 1 to 200 characters. - `ask` (`string`, required): What Bash does on each run, in plain words. Limits: 1 to 8000 characters. - `every` (`string`): When it runs, in plain words: `weekday at 08:00`, `mon, wed, fri at 18:30`, `30 minutes`, `month on the 1st at 09:00`. Give one of `every`, `schedule` or `on`. Limits: 1 to 100 characters. - `schedule` (`object`): When it runs, as cron. - `cron` (`string`, required): A cron expression: `0 7 * * *` runs at 7:00 every day. Limits: 1 to 120 characters. - `timezone` (`string`): The zone the cron is read in, an IANA name such as `Asia/Kolkata`. Limits: 1 to 64 characters. - `on` (`"call"`): `call`: it runs whenever your system posts to its `trigger_url`. - `filters` (`map of any`): With `on: call`, what a call's payload must match for the routine to run. - `timezone` (`string`): The zone `every` is read in, an IANA name. Absent: each user's own (or your company's own Bash's), then UTC. Limits: 1 to 64 characters. - `for` (`"us" | "each customer"`, required): `us`: your company's own Bash. `each customer`: a copy for every user, made the first time they talk to Bash here. - `enabled` (`boolean`): false keeps the routine but stops its runs. A routine for each user is on while it is in the file. - `limits` (`object`): What one task may spend. Default: `{"per_task_usd":2}`. ```yaml limits: { per_task_usd: 5 } ``` - `per_task_usd` (`number`): The most one task may spend, in dollars: above 0, at most 100. Absent: 2. Default: `2`. Limits: above 0, at most 100. - `tests` (`object[]`): Tests each new version runs before it is promoted. At most 20. Default: `[]`. Limits: at most 20 items. ```yaml tests: - name: Bag allowance ask: "How many kilos can I take on booking TN-48213?" expect: Says 7 kg of cabin baggage. tool_results: { get_booking: { cabin_kg: 7 } } ``` - `name` (`string`): Its name, which finds the test again on the next apply. Absent: the start of `ask`. Limits: 1 to 120 characters. - `ask` (`string`, required): What the test user wants, in their words. Limits: 1 to 2000 characters. - `expect` (`string | string[]`, required): What a pass looks like: one line, or up to 12. - `never` (`string | string[]`): What must never happen: one line, or up to 12. - `persona` (`string`): Who the test user is. Absent: a person who uses the company's app. Limits: 1 to 2000 characters. - `tool_results` (`map of any`): What each tool answers in this test: a function tool by its name, and a tool of an MCP server as . (the server's slug, from its URL's host), since a test never runs one for real unless the server marks it read-only. A tool with none answers an error. - `approves` (`boolean`): true: the test user says yes to each approval for a tool of your MCP server, which plays its `tool_results` entry or does not run, so the test can check what Bash does after a yes. Absent or false: it says no to every approval. Anything else that waits for a yes is always declined. - `webhooks` (`map of (string | "*")[]`): Where events go: each https URL, with the events it gets, or `*` for every one. Default: `{}`. ```yaml webhooks: "https://example.com/vestra": - task.completed - interaction.created ``` --- url: https://vestra.ai/developers/reference/events.md --- # Events reference > One catalog names every event. A conversation's stream carries what happens as it happens; your webhooks get what settles. Each event says where it arrives, what it means, and every field of its `data`. ## How each arrives The SDKs hand you both as one event: `event.type`, `event.user` and `event.data`. On the stream: - `seq` (`integer`): Its place on the conversation's stream, counting up: send the last one you saw to pick up after it. - `type` (`string`): What happened, from the one catalog of events: the events reference says what each carries and where it is delivered. Limits: one of 34 values. Example: `message.completed`. - `data` (`map of any`): What it says: its fields depend on its type, as the events reference lists them. - `created_at` (`string`): When it happened, as an ISO 8601 time. In a webhook: - `id` (`string`): The event (evt_...): the same on every retry, so use it to skip one you already took. Example: `evt_4hq`. - `object` (`"event"`): Always event. - `type` (`string`): An event a webhook carries: every event of the catalog but the six that ride only the stream (message.delta, task.progress, usage.updated, screen.control, error and ping). Limits: one of 28 values. Example: `task.completed`. - `mode` (`"live" | "test"`): The mode it happened in: test or live. - `created_at` (`string`): When it happened, as an ISO 8601 time. - `user_id` (`string | null`): Your id for the user it concerns; null for your company's own Bash, or when it concerns no user. - `task_id` (`string | null`): The task it concerns (task_...); null when none. - `conversation_id` (`string | null`): The conversation it concerns (conv_...); null when none. - `data` (`map of any`): What it says: its fields depend on its type, as the events reference lists them. ## The reply ### `message.delta` The reply as it is written, a piece at a time. Arrives: stream only. - `text` (`string`): The next piece of the reply, as it is written. - `turn_id` (`string`, may be absent): The turn it belongs to (turn_...). - `task_id` (`string`, may be absent): For words a task writes, rather than Bash's reply: the task (task_...). ### `message.completed` The reply is complete: its answer (its final words, after any search), its whole text, and what the turn cost. An empty text means the turn only started work: `task.started` names it. Arrives: stream and webhook. - `text` (`string`): The whole reply. For a message sent straight to work: Started work on it. Empty when Bash started work without a word. - `answer` (`string | null`, may be absent): The reply's answer: its final words, the part after any search or lookup Bash made on the way, trimmed; null when it said nothing. Absent from older events. - `turn_id` (`string`, may be absent): The turn (turn_...). - `task_id` (`string`, may be absent): For a task's words (every step's, joined), rather than Bash's reply: the task (task_...). - `data` (`unknown`, may be absent): The data you asked for with output_schema, when the reply carries it. - `error` (`{ code: string; message: string }`, may be absent): Why the turn failed, as a code and a message in plain words; absent when it did not. - `usage` (`{ turn_cost_usd: number; conversation_total_usd: number; cost_complete: boolean } | null`, may be absent): What the turn has cost (turn_cost_usd), the conversation's total, and whether every charge is in (cost_complete); null when it could not be read. ## The work ### `task.started` Bash started work on its computer: the task's id and title. Arrives: stream and webhook. - `task_id` (`string`): The task Bash started (task_...). - `title` (`string | null`): What the work is, in a few words; null until Bash names it. - `turn_id` (`string`, may be absent): The turn that started it (turn_...). ### `task.progress` A step of the work, or a change in its status. Arrives: stream only. - `progress` (`'step' | 'status'`): step: a step of the work (a tool used, or its to-do list). status: where the work is (working, waiting). - `task_id` (`string`, may be absent): The task (task_...), on a conversation's stream. - `turn_id` (`string`, may be absent): The turn (turn_...), when Bash's reply took the step. - `status` (`string`, may be absent): For a status: working, or waiting on an approval or a question. - `tool` (`string`, may be absent): For a step Bash's reply took: the tool it used. - `todos` (`{ content: string; status: string; }[]`, may be absent): For a to-do list: each step of the plan, with its content and its status (pending, in_progress, completed). ### `task.completed` The work is done: its answer (its final reply), its summary, and what it cost. Arrives: stream and webhook. - `task_id` (`string`): The task (task_...). - `status` (`string`, may be absent): How it ended, on the conversation's stream: completed, failed or stopped. - `cost_usd` (`number | null`, may be absent): What it cost, in billed US dollars; null when not known yet. - `cost_complete` (`boolean`, may be absent): Whether every charge is in: false when the last ones can still land, on usage.updated. - `summary` (`string | null`, may be absent): Everything the work said, every step's words joined: its narration and its final reply together; null when it said nothing. - `answer` (`string | null`, may be absent): The work's answer: its final reply, the words of its last step that said anything; null when none did. Absent from older events. ### `task.failed` The work failed: why, and what it cost. Arrives: stream and webhook. - `task_id` (`string`): The task (task_...). - `status` (`string`, may be absent): How it ended, on the conversation's stream: completed, failed or stopped. - `cost_usd` (`number | null`, may be absent): What it cost, in billed US dollars; null when not known yet. - `cost_complete` (`boolean`, may be absent): Whether every charge is in: false when the last ones can still land, on usage.updated. - `reason` (`string`, may be absent): Why it failed, as a short code, such as disconnected. - `message` (`string | null`, may be absent): More about why, in plain words; null when there is no more. - `title` (`string`, may be absent): What the work was, when it failed before it started. - `turn_id` (`string`, may be absent): The turn that tried to start it (turn_...), when it failed before it started. ### `task.stopped` The work was stopped: by you or a person, or by a limit, the credits running out or a policy, which its reason names. Arrives: stream and webhook. - `task_id` (`string`): The task (task_...). - `status` (`string`, may be absent): How it ended, on the conversation's stream: completed, failed or stopped. - `cost_usd` (`number | null`, may be absent): What it cost, in billed US dollars; null when not known yet. - `cost_complete` (`boolean`, may be absent): Whether every charge is in: false when the last ones can still land, on usage.updated. - `reason` (`string`, may be absent): Why it stopped, when a limit or a policy stopped it rather than a person: max_turns, max_budget, credits_exhausted (the account's credits ran out) or hook_blocked. - `message` (`string | null`, may be absent): More about why, in plain words (a policy hook's own words); null when there is no more. ## What Bash waits on ### `interaction.created` Bash waits on someone: an approval, a question, an app to connect, a hand-off, or a call to your tool. Arrives: stream and webhook. - `interaction_id` (`string`): What Bash waits on, to answer by its id (int_..., or call_... for a call to your tool). - `type` (`InteractionType`): What answer it takes: approval, question, connect, handoff or tool_call. - `task_id` (`string`, may be absent): The task that asked (task_...). - `turn_id` (`string`, may be absent): For a call to your tool from Bash's reply: the turn (turn_...). - `name` (`string`, may be absent): For a call to your tool: the tool's name. - `arguments` (`unknown`, may be absent): For a call to your tool: what Bash passed it. - `server` (`string`, may be absent): For a call to a tool of your MCP server in a test run, which Bash never runs for real there: the server's slug. ### `interaction.resolved` What Bash waited on was answered, or withdrawn. Arrives: stream and webhook. - `interaction_id` (`string`, may be absent): What was answered, withdrawn or closed (int_... or call_...). Absent when interaction is sent instead. - `interaction` (`Interaction`, may be absent): For an ask that settles without waking the work (a hand-off's verdict, a question the server asked), answered or withdrawn: the whole interaction, with its outcome. Webhook only. - `type` (`InteractionType`, may be absent): For a call to your tool: tool_call. - `task_id` (`string`, may be absent): The task that asked (task_...). - `outcome` (`string`, may be absent): cancelled: its task's stop or failure, or a newer message, closed it unanswered. Absent when it was answered or withdrawn. - `reason` (`string`, may be absent): With outcome cancelled: stopped or failed (its task ended), or superseded (a new message came in). - `status` (`string`, may be absent): For a call to your tool: completed with your output, or failed with your error. ### `interaction.expired` A call to your tool went unanswered for too long. Arrives: stream and webhook. - `interaction_id` (`string`): What expired unanswered (int_... or call_...). - `type` (`InteractionType`, may be absent): What answer it took. - `task_id` (`string`, may be absent): The task that asked (task_...). ## The screen ### `screen.control` Who has the screen: a person took the wheel, or Bash has it back. Arrives: stream only. - `by` (`'user' | 'bash'`): Who drives the screen now: user, the person, or bash. - `session_id` (`string`, may be absent): The session the person drives from (ses_...). ## Sessions ### `session.started` A session opened: a live screen, a voice call, or Bash inside your app. Arrives: stream and webhook. - `session_id` (`string`): The session (ses_...). - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone. - `chat` (`boolean`): Whether it shows the conversation. - `screen` (`false | 'view' | 'control'`): Its screen: view, control, or false for none. - `voice` (`boolean`): Whether your user can talk by voice. - `memory` (`boolean`): Whether it shows what Bash remembers. - `expires_at` (`string`): When its token stops working, as an ISO 8601 time. ### `session.ended` A session ended. A voice call's carries its `seconds` and its `transcript`. Arrives: stream and webhook. - `session_id` (`string | null`): The session (ses_...); null for a call that no session opened. - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `conversation_id` (`string | null`): Its conversation (conv_...); null when it had none. - `end_reason` (`string`): Why it ended: revoked when you ended the session, otherwise how its call ended, such as lost. - `seconds` (`number`, may be absent): For a call: how long it lasted, in seconds. - `transcript` (`{ speaker: "user" | "bash"; text: string; }[]`, may be absent): For a call: what was said, in order, by user or bash. - `transcript_truncated` (`boolean`, may be absent): For a call: true when the transcript stops at its 100,000 characters. ## Files ### `file.created` Bash made a file: its id, its name and its task. Arrives: stream and webhook. - `file_id` (`string`): The file (file_...): read it with GET /v1/files/{file_id}. - `task_id` (`string`, may be absent): The task that made it (task_...). - `filename` (`string | null`): Its name; null when it has none. - `mime_type` (`string | null`): Its type, such as application/pdf; null when unknown. - `size_bytes` (`number | null`): Its size, in bytes; null when unknown. ## Routines ### `routine.fired` A routine ran, on its schedule or on your call. Arrives: stream and webhook. - `routine_id` (`string`): The routine (rtn_...). - `run_id` (`string`): This run of it. - `conversation_id` (`string`): The conversation the run posts into (conv_...). - `user_id` (`string | null`): Your id for its user; null on your company's own Bash. - `turn_id` (`string`): The turn the run started (turn_...). ## Money ### `usage.updated` Charges that landed after a turn settled, for the turn and its work. Arrives: stream only. - `delta_usd` (`number`): What was just charged, in billed US dollars. - `task_id` (`string`, may be absent): The task it was charged to (task_...), when it was. - `task_total_usd` (`number`, may be absent): That task's total so far, in billed US dollars. - `conversation_total_usd` (`number | null`): The conversation's total so far, in billed US dollars; null when not known. - `user` (`{ day: { spent_usd: number; cap_usd: number | null; remaining_usd: number | null }; month: { spent_usd: number; cap_usd: number | null; remaining_usd: number | null } } | null`): The spend, cap and what is left, today and this month, of the user the charge was billed to (your company's own Bash counts as one); null when the standing could not be read. - `as_of` (`string`): When these numbers were read, as an ISO 8601 time. ### `usage.threshold_reached` Spend passed one of your alert thresholds. Arrives: webhook only. - `scope` (`'company' | 'user'`): Whose spend: company, all of your company's, or user, one user's. - `user_id` (`string`, may be absent): For a user's alert: your id for the user. - `period` (`'day' | 'month'`): The period the cap is for: day or month (UTC). - `period_start` (`string`): When the period started, as an ISO 8601 time. - `threshold_pct` (`number`): The share of the cap spent, in percent: one of your alert thresholds, or 100 at the cap. - `spent_usd` (`number`): What has been spent in the period, in billed US dollars. - `cap_usd` (`number`): The cap, in US dollars. ### `usage.cap_reached` Spend reached one of your caps. Arrives: webhook only. - `scope` (`'company' | 'user'`): Whose spend: company, all of your company's, or user, one user's. - `user_id` (`string`, may be absent): For a user's alert: your id for the user. - `period` (`'day' | 'month'`): The period the cap is for: day or month (UTC). - `period_start` (`string`): When the period started, as an ISO 8601 time. - `threshold_pct` (`number`): The share of the cap spent, in percent: one of your alert thresholds, or 100 at the cap. - `spent_usd` (`number`): What has been spent in the period, in billed US dollars. - `cap_usd` (`number`): The cap, in US dollars. ## The conversation ### `conversation.status` The conversation's status (`idle`, `replying`, `working` or `waiting`) and its turn's: `turn_status` is `running`, `working`, `requires_action`, `completed`, `failed` or `stopped`. Webhooks carry the turns that settle. Arrives: stream and webhook. - `status` (`'idle' | 'replying' | 'working' | 'waiting'`): What the conversation is doing now: idle, replying, working or waiting. - `turn_id` (`string`, may be absent): The turn that changed it (turn_...). - `turn_status` (`'running' | 'working' | 'requires_action' | 'completed' | 'failed' | 'stopped'`, may be absent): Where that turn is now: running, working, requires_action, or settled as completed, failed or stopped. - `data` (`unknown`, may be absent): When the turn settled: the data you asked for with output_schema. - `outcome` (`{ met: boolean; reason: string }`, may be absent): When the turn settled: whether it met your success_criteria (met), and why (reason). - `error` (`{ code: string; message: string }`, may be absent): When the turn failed: why, as a code and a message. - `usage` (`{ turn_cost_usd: number; conversation_total_usd: number; cost_complete: boolean } | null`, may be absent): When the turn settled: what it cost, the conversation's total, and whether every charge is in. ## Users ### `user.created` A user was made, the first time you named them. Arrives: webhook only. - `user_id` (`string`): Your id for the user Vestra just made, the first time you named them. ### `user.deleted` A user's erasure finished: their Bash, computer, memory and files are gone. Arrives: webhook only. - `user_id` (`string`): Your id for the user who was erased. - `complete` (`boolean`, may be absent): Whether every part was erased; false when a part could not be, and it is retried. ## Connected apps ### `connection.completed` An app was connected. Arrives: webhook only. - `user_id` (`string | null`): Your id for the user who connected; null on your company's own Bash. - `connection` (`{ object: 'connection'; id: string; user_id: string | null; app: string; status: string; label: string | null; created_at: string }`): The connection made: its id (con_...), app, status, label and when it was made. ### `connection.failed` Connecting an app failed. Arrives: webhook only. - `user_id` (`string | null`): Your id for the user; null on your company's own Bash. - `app` (`string`): The app that did not connect. - `connection_id` (`string | null`): The connection that failed (con_...); null when none was made. - `reason` (`string | null`): Why, in plain words; null when the app did not say. ### `connection.removed` An app was disconnected. Arrives: webhook only. - `user_id` (`string | null`): Your id for the user; null on your company's own Bash. - `connection_id` (`string`): The connection removed (con_...). - `app` (`string`): Its app. ## Memory ### `memory.observed` Bash learned something new to remember about a user. Arrives: webhook only. - `user_id` (`string`): Your id for the user Bash learned about. - `task_id` (`string`): The task it learned it in (task_...). - `conversation_id` (`string`, may be absent): That task's conversation (conv_...). - `observation` (`string`): What Bash learned, in plain words: keep it in your memory, which your memory endpoint gives back. - `kind` (`string`): What kind of fact it is, such as preference or identity. - `source` (`string`): How Bash learned it, such as agent_inferred. - `observed_at` (`string`): When it learned it, as an ISO 8601 time. ## Batches ### `batch.completed` Every item of a batch has finished. Arrives: webhook only. - `batch_id` (`string`): The batch (bat_...). - `status` (`'completed' | 'cancelled'`): completed: every item finished. cancelled: you cancelled it. - `item_count` (`number`): How many items it had. - `counts` (`{ queued: number; running: number; succeeded: number; failed: number; cancelled: number }`): Its items, by status: queued, running, succeeded, failed and cancelled. ## Deployments ### `deployment.version_created` A deployment has a new version. Arrives: webhook only. - `deployment_id` (`string`): The deployment (dep_...). - `version` (`number`): The new version. - `manifest_sha256` (`string`): A fingerprint of the version's file: two versions with the same one are the same setup. ### `deployment.promoted` A version went live in its mode. Arrives: webhook only. - `deployment_id` (`string`): The deployment (dep_...). - `from_version` (`number | null`): The version it ran before; null when none did. - `to_version` (`number`): The version it runs now. - `became_default` (`boolean`): Whether this promotion made the deployment your company's default. ### `deployment.rolled_back` A deployment went back to an earlier version. Arrives: webhook only. - `deployment_id` (`string`): The deployment (dep_...). - `from_version` (`number | null`): The version it ran before; null when none did. - `to_version` (`number`): The version it runs now. ## Tests ### `test_run.completed` A test run finished, with how many scenarios passed. Its `error_code` is null unless the run ended in `error`: `interrupted` when it stopped reporting. Arrives: webhook only. - `test_run_id` (`string`): The test run (trun_...). - `deployment_id` (`string`): The deployment it tested (dep_...). - `version` (`number`): The version it tested. - `status` (`'passed' | 'failed' | 'error'`): passed: every test passed. failed: at least one did not. error: the run could not finish. - `error_code` (`string | null`): Why the run could not finish, such as interrupted; null unless its status is error. - `passed` (`number`): How many tests passed. - `total` (`number`): How many tests it played. - `promoted` (`{ mode: 'live' | 'test'; version: number } | null`): What the pass promoted, its mode and version; null when it promoted nothing. - `became_default` (`boolean`): Whether that promotion made the deployment your company's default. ## Keys ### `key.created` A key was made. Arrives: webhook only. - `key_id` (`string`): The key (key_...). - `name` (`string | null`, may be absent): Its name; null when it has none. ### `key.revoked` A key was revoked. Arrives: webhook only. - `key_id` (`string`): The key that stopped working (key_...). ## The stream itself ### `error` The stream's own error: its code and its message. Arrives: stream only. - `code` (`string`): A stable code to branch on. - `message` (`string`): What went wrong, in plain words. - `turn_id` (`string`, may be absent): The turn it happened in (turn_...). ### `ping` A keep-alive on the stream: nothing happened. Arrives: stream only. No data: it says only that it happened. --- url: https://vestra.ai/developers/reference/errors.md --- # Errors reference > Every error answers one body. Branch on `code`, never on the message; show the person `message` when it helps; and send us `request_id` when it is our side. ## The body - `error` (`object`): The error. - `type` (`string`): The kind of error, one per status: `invalid_request`, `authentication`, `permission`, `not_found`, `conflict`, `rate_limited`, `cap_reached`, `upstream` or `internal`. Limits: one of 9 values. - `code` (`string`): What went wrong, exactly: branch on this. The errors reference lists every code, with what to do. Example: `user_not_found`. - `message` (`string`): What went wrong, in a sentence a person can read. It may change: never branch on it. - `param` (`string`, may be absent): The field the error is about, as a path such as `tools.0.input`; absent when it is about no one field. - `names` (`string[]`, may be absent): With `secret_missing`: the company secrets to store before applying the file again. - `deployments` (`string[]`, may be absent): With `mcp_server_shared`: the other deployments that run the MCP server the file would change. - `partial` (`object`, may be absent): With `timeout` on a browser fetch: what the page had sent before the time ran out. - `retry_after_ms` (`integer`, may be absent): With `browser_busy`: how long to wait before trying again, in ms. - `request_id` (`string`): This request's id: send it to Vestra when the error is on our side. ```json { "error": { "type": "not_found", "code": "user_not_found", "message": "No such user.", "request_id": "req_8f2k" } } ``` ## 400: The request is not one the API takes - `answer_mismatch`: The answer's shape is not one this interaction takes. What to do: Send the body its type takes: an approval `approve`, a question `answers`, a call to your tool `output` or `error`. - `answers_mismatch`: The answers do not match the questions. What to do: Send exactly one answer for each question, in the order they were asked. - `answers_not_expected`: This interaction takes a yes or a no, not answers. What to do: Send `{ "approve": true }` or `{ "approve": false }`. - `answers_required`: This question waits for answers. What to do: Send `answers`, one for each question, or `{ "approve": false }` to decline. - `app_not_connectable`: This app cannot be connected yet. What to do: Pick another app from `GET /v1/apps`. - `auth_twice`: An MCP tool in the file gives both `auth` and an Authorization header. What to do: Keep one of the two. - `binary_file`: A skill holds a file that is not text. What to do: Skills take text files only: leave out images and other binaries. - `body_too_large`: The request body, or a context inside it, is larger than this route takes. What to do: Send less, or upload the data as a file with `POST /v1/files` and name it. - `bundle_too_large`: A skill is larger than 5 MB. What to do: Make the skill smaller: keep what Bash reads, and leave out the rest. - `cap_above_maximum`: A cap is above what is allowed: the company cap above its ceiling, or a user cap above the company cap. What to do: Lower the cap, or ask Vestra to raise the ceiling. - `connect_app_instead`: This interaction is an app to connect, and it is not answered with a body. What to do: Connect the app with `POST /v1/connections`: the interaction settles itself once the app is connected. - `conversation_required`: The MCP tool needs a conversation and none was named. What to do: Name the conversation to act on. - `conversation_user_mismatch`: The conversation belongs to a different user than the one named. What to do: Name the conversation of the same user, or leave the user out. - `decision_not_expected`: A hand-off answer was sent to something that is not a hand-off, or the other way round. What to do: Send `done`, `could_not` or `not_now` to a hand-off only, and the body its type takes to the others. - `deployment_required`: This call needs a deployment and none was named. What to do: Name the deployment by its name or its `dep_` id. - `describe_or_answer`: The MCP `init` tool needs a description to start, or a deployment, a conversation and an answer to go on. What to do: Start with `describe`, then answer with `deployment`, `conversation` and `answer`. - `document_too_large`: A notebook document is larger than 512 KB of text. What to do: Split it into several documents. - `duplicate_name`: The file names two tools, tests or routines the same. What to do: Give each its own name. - `duplicate_path`: Two files of a skill share one path. What to do: Give each file its own path. - `file_required`: The upload has no file in it. What to do: Send the file as multipart/form-data, in a field named `file`. - `file_too_large` (also 413): A file is larger than its route takes: a file of a skill over 1 MB, or an upload over its limit. What to do: Send a smaller file. - `install_ability_instead`: Bash asked to install an ability, and that is not approved through the API. What to do: Add the skill or MCP server to your deployment instead; this interaction can only be declined. - `invalid_conversation_id`: `conversation_id` is not a conversation id. What to do: Send a `conv_` id, as the API gave it. - `invalid_cursor`: The cursor is not one this API gave. What to do: Send the `next_cursor` of the page before, unchanged. - `invalid_deployment`: That is not a deployment id or name. What to do: Send a `dep_` id, or the name, with an optional `@version`. - `invalid_deployment_id`: `deployment_id` is not a deployment id. What to do: Send a `dep_` id, as the API gave it. - `invalid_filename`: The file name is not one plain name. What to do: Use one name with no slashes or control characters, at most 255 bytes. - `invalid_frontmatter`: A skill's SKILL.md frontmatter is not valid YAML. What to do: Fix the YAML between the two `---` lines at the top of SKILL.md. - `invalid_idempotency_key`: The Idempotency-Key header is empty or too long. What to do: Send 1 to 255 characters. - `invalid_json`: The request body is not valid JSON. What to do: Send JSON, with `Content-Type: application/json`. - `invalid_language`: That is not a language tag. What to do: Use `auto`, or a tag such as `en`, `hi` or `pt-BR`. - `invalid_last_event_id`: `Last-Event-ID` or `after` is not a whole number. What to do: Send the `seq` of the last event you saw, or 0 for the start. - `invalid_limit`: `limit` is not a whole number from 1 to 100. What to do: Send a `limit` from 1 to 100, or leave it out. - `invalid_manifest`: The deployment's settings do not fit together: the message and `param` say where. What to do: Fix the field `param` names and send it again. - `invalid_memory`: The memory is not one Bash can keep. What to do: Send `text` of 1 to 1,000 characters, and optionally a `kind`: identity, relationship, commitment, preference, project_state or other. - `invalid_mode`: The `Vestra-Mode` header is not live or test. What to do: Send `live` or `test`. - `invalid_org_header`: The `Vestra-Org` header is not an org id. What to do: Send your org id, like `org_...`. - `invalid_origin`: An allowed origin is not an https origin. What to do: Write it as `https://app.example.com`, with no path. - `invalid_query`: A query field is not valid: `param` names it. What to do: Fix that field, or leave it out. - `invalid_repo`: A repository in the file is not an address Bash can clone. What to do: Write `github.com/owner/repo`, or its https URL. - `invalid_request`: The request is not valid: the message and `param` say which field and why. What to do: Fix that field and send it again. The route says what each field takes. - `invalid_schedule`: The schedule is not one a routine can keep: an unknown time zone, or a cron that does not read. What to do: Use an IANA time zone such as `Asia/Kolkata`, and a cron such as `0 7 * * *`, or write `every` in plain words. - `invalid_secret_name`: That is not a secret name. What to do: Use capitals, digits and underscores, starting with a capital: `PNR_API_KEY`. - `invalid_skill`: A skill's SKILL.md does not meet the skill format. What to do: Fix what the message names, and upload the skill again. - `invalid_task_id`: `task_id` is not a task id. What to do: Send a `task_` id, as the API gave it. - `invalid_tool_input`: A function tool's `input` is neither shorthand nor JSON Schema. What to do: Write each argument as a type such as `string` or `integer?`, or give JSON Schema with `type: object`. - `invalid_trigger`: A routine's `filters` are not ones it can match. What to do: Fix the filter the message names. - `invalid_user_timezone`: The user's own time zone is not an IANA name, so `every` cannot be read in it. What to do: Send `timezone`, or fix the user with `PATCH /v1/users/{user_id}`. - `invalid_window`: `from` is after `to`, or one of them is not a date. What to do: Send dates like `2026-10-01`, with `from` on or before `to`. - `invalid_zip`: The skill upload is not a zip archive that reads. What to do: Zip the skill folder again and upload it. - `mcp_server_not_listed`: A tool policy names an MCP server the version does not list. What to do: Add the server to `mcp_servers`, or drop its policy. - `memory_endpoint_required`: Memory kept on your side needs an endpoint for Bash to ask. What to do: Send `memory_endpoint` with `memory_mode: customer`. - `message_required`: There is nothing to send: no message, attachment or tool result. What to do: Send a `message`, an attachment or tool results. - `multipart_required`: The upload is not multipart/form-data. What to do: Send the file as multipart/form-data, in a field named `file`. - `name_mismatch`: The file's `name` and the name in the path differ. What to do: Make them the same. - `no_output_schema`: This work was not asked for structured output, so it has no result to read. What to do: Ask with `output_schema` when you want a result to read. - `nonce_mismatch`: An approval that moves money or acts for the user needs its nonce, and it was missing or wrong. What to do: Read the interaction and send its `nonce` with `approve: true`. - `not_a_schedule_routine`: This routine runs on a call, so it has no schedule to change. What to do: Change its filters, or make a new routine that runs on a schedule. - `not_a_trigger_routine`: This routine runs on a schedule, so it takes no payload and has no filters. What to do: Run it with `POST /v1/routines/{routine_id}/run`. - `notebook_full`: The notebook is full: too many documents, or more than 5 MB of text. What to do: Delete documents, or start another notebook. - `oauth_not_available`: OAuth for company MCP servers is not available yet. What to do: Send headers with `${user_secret:NAME}` for each user's own credentials. - `output_schema_invalid`: `output_schema` is not a schema Bash can follow. What to do: Send a JSON Schema object; the message says what is wrong with it. - `output_schema_too_large`: `output_schema` is larger than 32 KB. What to do: Send a smaller schema. - `path_not_resolved`: The file names a folder or file on your computer, and HTTP cannot read it. What to do: Deploy with the CLI or an SDK: they upload it and send what it became. - `payload_too_large`: A routine's trigger payload is larger than 4 KB. What to do: Send less, or put the data in a file and name it. - `present_user_required`: This approval moves money or acts for the user, and the user must be there to say yes. What to do: Approve it only while the user is in your app, and send `present_user: true`. - `query_required`: A memory search needs a question. What to do: Send `q`, 1 to 500 characters. - `raw_secret_in_header`: An MCP header holds a raw credential. What to do: Store it with `PUT /v1/secrets/{name}` and write `${secret:NAME}` in the header. - `return_url_not_allowed`: `return_url` is not one of your allowed return URLs. What to do: Add it with `PATCH /v1/org`, or send one that is allowed. - `schedule_too_frequent`: A routine runs at most once every 15 minutes. What to do: Choose a schedule that runs less often. - `schedule_unreadable`: `every` does not read as a schedule. What to do: Write it like `weekday at 08:00`, `mon, wed, fri at 18:30` or `30 minutes`. - `sdp_required`: A voice call needs the browser's WebRTC offer. What to do: Send the offer as `sdp`. - `secret_in_url`: An MCP server's URL carries a credential. What to do: Move it into a header as `${secret:NAME}`. - `secret_missing`: The file names company secrets that are not stored for this mode: `names` lists them. What to do: Store each with `PUT /v1/secrets/{name}` and a key of this mode, then apply the file again. - `skill_md_missing`: A skill folder has no SKILL.md at its top. What to do: Add SKILL.md at the top of the folder. - `skill_name_changed`: A new version of a skill changes the name in its SKILL.md. What to do: Keep the name, or upload a new skill to rename it. - `skill_name_required`: A skill's SKILL.md has no name. What to do: Give the frontmatter a `name` that starts with a letter. - `title_required`: A raw text upload to a notebook needs a title. What to do: Send `?title=`. - `too_many_files`: A skill holds more files than it may. What to do: Keep the files Bash reads, and leave out the rest. - `too_many_headers`: An MCP server sends more than 20 headers. What to do: Send at most 20. - `tool_listed_twice`: A tool is in more than one of an MCP server's lists. What to do: Put each tool in one list: `deny`, `require_approval` or `run_without_asking`. - `tool_name_reserved`: A tool, or an MCP server's slug, takes a name Bash already uses for one of its own tools or tool servers. What to do: Name it something else. - `tool_not_declared`: A test gives results for a tool the file does not declare. What to do: Declare the tool in `tools`, or drop its results from the test's `tool_results`. - `tool_not_on_server`: An MCP server's `run_without_asking` names a tool the server does not list. What to do: Use each name exactly as the server lists it (`GET /v1/mcp-servers/{server_id}/tools`), or drop it. - `tool_policy_twice`: Two tool policies name the same MCP server. What to do: Give each MCP server one policy. - `tool_schema_invalid`: A tool's input schema is not a JSON Schema object. What to do: Give it `type: object` and its `properties`. - `unknown_model`: That is not a model a deployment can use. What to do: Use `default`, or an id from `GET /v1/models`. - `unsafe_path`: A file of a skill has a path that is not allowed. What to do: Use plain relative paths, with no `..` and no leading slash. - `unsupported_kind`: This kind of interaction cannot be answered, or withdrawn, through the API. What to do: Decline it, or let the user choose in Vestra. - `url_not_public`: The URL is not a public https address. What to do: Use an https URL that reaches the public internet. - `user_must_connect`: The user signs in to this app themselves. What to do: Send the user the connect link. Through the API, this interaction can only be declined. - `user_required`: This company asks for a user on every request, and none was named. What to do: Name the user, or turn off `require_user` with `PATCH /v1/org`. - `version_not_allowed`: This call runs the current version, so it takes no `@version`. What to do: Drop the `@version`. - `voice_unknown`: That is not one of the voices. What to do: Use one of the voices the message lists. - `window_too_long`: The usage window is longer than the API reads at once. What to do: Ask for a shorter window, and page through it. ## 401: Who is calling is not known - `invalid_api_key`: That API key is not valid. What to do: Send a key from the console, as `Authorization: Bearer vk_test_...`. - `invalid_session`: The console session is not valid. What to do: Sign in again. - `key_expired`: This API key has expired. What to do: Use the key that replaced it. - `key_revoked` (also 409): This API key was revoked: it calls nothing, and it cannot be rolled. What to do: Make a new key in the console or with `POST /v1/keys`. - `missing_api_key`: The request has no API key. What to do: Send `Authorization: Bearer vk_test_...` (or `vk_live_...`). - `missing_session`: The console request has no session. What to do: Send your Vestra session as `Bearer `. - `org_header_required`: A console session must say which company it acts for. What to do: Send the `Vestra-Org` header with your org id. - `session_ended`: This session has ended, and its token stopped working. What to do: Ask your server for a new session with `POST /v1/sessions`. ## 402: Money stops the call - `org_cap_reached`: The company's spending cap is reached: its cap for the day (it resets at midnight UTC) or for the month (it resets on the 1st, UTC). What to do: `GET /v1/limits` says which. Raise it with `PATCH /v1/limits`, or wait for it to reset. - `spend_cap_reached`: A spending cap is reached, the user's or the company's, for the day or the month, so their computer was not woken, or the voice call did not open. What to do: `GET /v1/users/{user_id}` and `GET /v1/limits` say which. Raise it, or wait for it to reset: a day's cap at midnight UTC, a month's on the 1st. - `storage_quota_exceeded`: This user has used all of their file storage. What to do: Delete files with `DELETE /v1/files/{file_id}` to make room. - `user_cap_reached`: This user's spending cap is reached: their cap for the day (it resets at midnight UTC) or for the month (it resets on the 1st, UTC). What to do: `GET /v1/users/{user_id}` says what they may still spend. Raise their cap with `PATCH /v1/users/{user_id}`, or wait for it to reset. - `wallet_empty`: The wallet for this mode is empty. What to do: Add credits in the console, then retry. ## 403: Known, but not allowed - `account_blocked`: This account or user is blocked. What to do: Contact Vestra support. - `app_not_allowed`: This app is not on the deployment's list of apps. What to do: Add it to the deployment's `apps`, or pick an app that is there. - `client_token_not_allowed`: A session's token cannot call this route. What to do: Call it from your server, with an API key. - `domain_not_allowed`: The page is on a site the user's deployment does not list in `browser.allowed_domains`, or a browser fetch's page went to one. What to do: Add the site to the deployment's `browser.allowed_domains`, or open a page on a site that is there. - `email_required`: The signed-in Vestra account has no email address, and a company account is made for an email. What to do: Sign in with an account that has an email address, or contact Vestra support. - `insufficient_role`: This caller cannot do this: a console member without the role, or a session token. What to do: Ask an owner or admin, or call it with an API key. - `live_mode_required`: Only a live key changes what live users run. What to do: Use a live key, or the console in live mode. - `not_an_org_member`: You are not a member of this company account. What to do: Ask an owner to invite you, or choose a company you belong to. - `not_interview_computer`: Only the deployment's own interview can save its draft. What to do: Let the interview finish; it saves the draft itself. - `org_disabled`: This company account is switched off. What to do: Contact Vestra support. - `origin_not_allowed`: This page is not an allowed origin for this company. What to do: Add the origin to `allowed_embed_origins` with `PATCH /v1/org`. - `page_blocked`: The page is on the deployment's `browser.blocked_paths`, such as a payment page, so the browser does not open it or act on it. What to do: Leave payments to the person, on their own device. Change `browser.blocked_paths` on the deployment only if this page should be open to the browser. - `sandbox_token_required`: Only the interview computer can post this result. What to do: Nothing to do: this route is for Bash itself. - `scope_escalation`: A key cannot give a scope it does not have. What to do: Make the new key with a key that has the scopes, or ask for fewer. - `scope_missing`: This key's scopes do not reach this route. What to do: Make a key with the scope the message names, in the console or with `POST /v1/keys`. - `staff_only`: This route is for Vestra staff. What to do: Nothing to do: it is not part of the API you call. - `test_mode_required`: Only a test key does this: interviews, and changes to what test users run. What to do: Use a test key, or the console in test mode. - `user_disabled`: This user is switched off. What to do: Turn them back on with `PATCH /v1/users/{user_id}`. - `view_only`: This session can watch the screen but not drive it. What to do: Open the session with `screen: "control"` to take the wheel. ## 404: Not found - `batch_not_found`: No batch by that id, for this key's company and mode. What to do: Check the id, and that the key is of the mode the thing was made in: a test key never sees live data. - `connection_not_found`: No connected app by that id, for this key's company and mode. What to do: Check the id, and the user it belongs to: list them with `GET /v1/connections`. - `conversation_not_found`: No conversation by that id, for this key's company and mode. What to do: Check the id and the key's mode. A session token reaches only its own conversation. - `delivery_not_found`: No webhook delivery by that id, for this key's company and mode. What to do: List the deliveries with `GET /v1/webhooks/{endpoint_id}/deliveries`. - `deployment_not_found` (also 400): No deployment by that id, for this key's company and mode. What to do: Check the name or `dep_` id. Inside a file or a batch, it answers 400 on the field that names it. - `deployment_version_not_found`: No deployment version by that id, for this key's company and mode. What to do: List the versions with `GET /v1/deployments/{deployment_id}/versions`. - `document_not_found`: No notebook document by that id, for this key's company and mode. What to do: List the documents with `GET /v1/notebooks/{notebook_id}/documents`. - `event_not_found`: No event by that id, for this key's company and mode. What to do: List the recent ones with `GET /v1/events`. - `file_not_found` (also 400): No file by that id, for this key's company and mode. What to do: Check the id. A file attached to a message answers 400 on that field. - `interaction_not_found`: No interaction by that id, for this key's company and mode. What to do: List what Bash waits on with `GET /v1/interactions`. A session token reaches only its own. - `key_not_found`: No API key by that id, for this key's company and mode. What to do: List the keys with `GET /v1/keys`. - `mcp_server_not_found` (also 400): No MCP server by that id, for this key's company and mode. What to do: List them with `GET /v1/mcp-servers`. Inside a version, it answers 400 on the field. - `memory_not_found`: No memory by that id, for this key's company and mode. What to do: List what Bash remembers with `GET /v1/memory`. - `notebook_not_found` (also 400): No notebook by that id, for this key's company and mode. What to do: List them with `GET /v1/notebooks`. Inside a version, it answers 400 on the field. - `org_not_found`: The company account behind this call was not found. What to do: Contact Vestra support. - `route_not_found`: No route has that method and path. What to do: Check the path against the API reference, and that it starts `/v1/`. - `routine_not_found`: No routine by that id, for this key's company and mode. What to do: List them with `GET /v1/routines`. - `scenario_not_found`: No test scenario by that id, for this key's company and mode. What to do: List them with `GET /v1/deployments/{deployment_id}/scenarios`. - `secret_not_found`: No secret by that name, for this mode and user. What to do: List the names with `GET /v1/secrets`. - `session_not_found`: No session by that id, for this key's company and mode. What to do: Check the id. A session ends when its token does. - `skill_not_found` (also 400): No skill by that id, for this key's company and mode. What to do: List them with `GET /v1/skills`. Inside a file or a version, it answers 400 on the field. - `task_not_found`: No task by that id, for this key's company and mode. What to do: List the work with `GET /v1/tasks`. - `test_run_not_found`: No test run by that id, for this key's company and mode. What to do: List them with `GET /v1/deployments/{deployment_id}/test-runs`. - `tool_call_not_found`: No call to your tool by that id, for this key's company and mode. What to do: Answer the id the interaction gave you. - `turn_not_found`: No turn by that id, for this key's company and mode. What to do: Check the conversation and the turn id. - `user_not_found` (also 400): No user by that id, for this key's company and mode. What to do: Users are made the first time you act for them, or with `POST /v1/users`. Reads never make one. - `webhook_endpoint_not_found`: No webhook endpoint by that id, for this key's company and mode. What to do: List them with `GET /v1/webhooks`. ## 405: Not a method this route takes - `method_not_allowed`: The MCP endpoint takes POST only. What to do: Send JSON-RPC to `POST /v1/mcp`. ## 409: The state does not allow it now - `already_a_member`: You already belong to a company account, or this email already made one: a person makes only one of their own. What to do: Open the developer console to use it. To join another, or if yours is gone, ask Vestra to add you. - `batch_not_running`: That batch has already finished. What to do: Read its items with `GET /v1/batches/{batch_id}/items`. - `browser_busy`: The user's browser is already running four browser fetches. What to do: Retry after `retry_after_ms`, about a second, or run them one after another. - `browser_in_use`: A person is using this browser right now, so a browser fetch yields to them. What to do: Retry in a few seconds, when they are done. - `browser_not_open`: Bash's browser is not open right now. What to do: Ask Bash for work that opens its browser, then try again. - `call_not_open`: The call ended before its line opened. What to do: Start the call again. - `conversation_busy`: Bash is still replying in this conversation. What to do: Wait for the reply, or stop it with `POST /v1/conversations/{conversation_id}/stop`. - `delivery_in_flight`: This delivery is being sent right now. What to do: Try again in a minute. - `deployment_archived`: This deployment is archived. What to do: Unarchive it with `PATCH /v1/deployments/{deployment_id}` first. - `deployment_busy`: Several versions were saved at once. What to do: Try again. - `deployment_changed`: The deployment changed while this ran. What to do: Read the deployment again, then retry. - `deployment_is_default`: This deployment is a default, for the company or for users, so it cannot be archived. What to do: Set another default with `PATCH /v1/org` or `PATCH /v1/users/{user_id}`, then archive it. - `deployment_not_live`: This deployment has no live version yet. What to do: Promote a version first, or call it with a test key. - `idempotency_in_progress`: A request with this Idempotency-Key is still running. What to do: Retry in a moment: you get its answer. - `idempotency_key_reused`: This Idempotency-Key was used for a different request. What to do: Use a new key for a new request. - `interaction_already_settled`: This interaction was already answered or withdrawn. What to do: Nothing to do: read it to see how it settled. - `limits_changed`: The limits changed while you saved. What to do: Read them with `GET /v1/limits` and retry. - `mcp_server_exists`: An MCP server with this slug already exists. What to do: Use the one there, or give a new slug. - `mcp_server_live`: A live deployment uses this MCP server. What to do: Change it with a live key, or as an owner or admin. - `mcp_server_shared`: Another deployment runs this MCP server, and the file would change its settings: `deployments` names them. What to do: Use the same headers in every file that names the server, or give this deployment its own server's URL. - `memory_not_kept`: This company keeps memory on its own side, so Bash holds none here. What to do: Read the memory from your own store. - `memory_off`: Memory is off for this company. What to do: Turn it on with `PATCH /v1/org` and a `memory_mode`. - `memory_panel_off`: Showing a user what Bash remembers is off for this company. What to do: Turn it on with `PATCH /v1/org` and `memory_panel: true`. - `no_open_tab`: A browser fetch with no url runs on the tab an earlier fetch kept open under its `tab` name, and none is kept under it now. What to do: Send url, with keep_open_ms to keep its tab for the next fetch. - `no_scenarios`: This deployment has no tests yet, so there is nothing to run. What to do: Add `tests` to vestra.yaml (or draft some with `POST /v1/deployments/{deployment_id}/scenarios/generate`), deploy, then run them. - `not_a_company_user`: This user cannot be erased through the API. What to do: Contact Vestra support. - `notebook_busy`: The notebook changed while this call ran. What to do: Retry it. - `notebook_exists`: A notebook with this slug already exists. What to do: Use the one there, or give a new slug. - `nothing_to_roll_back`: There is no earlier live version to go back to. What to do: Promote the version you want instead. - `org_routine_limit_reached`: The company has 200 active routines in this mode. What to do: Delete some; a copy for each user counts too. - `payment_in_progress`: The person is paying for something in their browser right now; screenshots and the screen are paused until the payment ends. What to do: Try again in a minute. - `routine_completed`: This routine has finished and does not run again. What to do: Make a new routine. - `routine_limit_reached`: This user, or your company's own Bash, has as many active routines as it may. What to do: Delete or pause one first. - `routine_name_taken`: A routine by this name already exists, and the file does not own it. What to do: Rename one of them. - `routine_not_linked`: This routine was not made through the API, so it has no conversation to run in. What to do: Make the routine with `POST /v1/routines`. - `routine_paused`: This routine is paused. What to do: Resume it with `PATCH /v1/routines/{routine_id}` and `enabled: true`, then run it. - `screen_control_unavailable`: Taking control of the screen is not switched on here yet. What to do: Open the session with `screen: "view"`. - `session_has_no_screen`: This session was opened without a screen. What to do: Open one with `screen: "view"` or `"control"`. - `session_has_no_voice`: This session was opened without voice. What to do: Open one with `voice: true`. - `shared_change`: Skills, knowledge and company MCP servers are not versioned yet: every version reads them as they are, so while a deployment runs a version live, a test key or a console developer cannot change them. `deployments` names the live ones. What to do: Change them with a live key (`vestra deploy --live`), which changes them for your users at once, or as an owner or admin. - `skill_exists`: A skill with this slug already exists. What to do: Upload a new version with `PUT /v1/skills/{skill_id}`, or give a new slug. - `slug_taken`: A deployment with this slug already exists. What to do: Choose another name, or send a `slug`. - `test_run_failed`: This version failed its latest test run, so it is not promoted. What to do: Fix it, run the tests again, then promote. - `test_run_in_progress`: A test run for this version is already running. What to do: Wait for its verdict with `GET /v1/test-runs/{test_run_id}`. - `test_run_required`: This version has no passing test run. What to do: Run the tests with `POST /v1/deployments/{deployment_id}/test-runs`, then promote. - `test_runs_busy`: Too many test runs are running: this company has as many as it may, or we do. What to do: Try again when one finishes. - `too_many_batches`: The company already runs as many batches as it may in this mode. What to do: Wait for one to finish, or cancel one. - `too_many_scenarios`: The deployment would hold more test scenarios than it may. What to do: Remove scenarios made by hand, or declare fewer tests. - `tool_call_not_pending`: That call to your tool is not waiting for a result. What to do: Nothing to do: it was answered, or it expired. - `user_create_in_progress`: Another request is making this user right now. What to do: Retry in a moment. - `version_never_live`: That version was never live, so there is nothing to roll back to. What to do: Promote it instead. - `version_not_live`: A live key always runs the live version, so it cannot pin one. What to do: Drop the `@version`, or pin it with a test key. ## 410: Gone - `stream_expired`: The events after that point are no longer kept. What to do: Read the conversation again with `GET /v1/conversations/{conversation_id}`, and follow its events from now. ## 413: Too large - `export_too_large`: This user holds more than an export carries. What to do: Ask Vestra support for the export. ## 415: Not a type this route reads - `unsupported_media_type`: The body is in a type this route does not read. What to do: Send JSON with `Content-Type: application/json`, or the type the route names. ## 429: Slow down - `export_in_progress`: Another export of this user is being built. What to do: Retry in a minute. - `rate_limited`: Too many requests. What to do: Slow down, and retry after the `Retry-After` seconds. - `routine_fire_budget`: This routine, or this company, has used its runs for this hour. What to do: Try again later. - `too_many_in_flight`: Too many requests at once. What to do: Wait for one to finish, then retry. - `too_many_streams`: Too many open streams. What to do: Close one, then retry. - `voice_busy`: Too many voice calls are open. What to do: Retry in a minute. - `warm_limit`: Many computers are already waking for this company. What to do: Retry in a moment. ## 5xx: Our side: retry - `auth_unavailable` (503): We could not check your credentials just now. What to do: Retry in a few seconds. - `box_unavailable` (503): The user's computer did not wake or did not answer, or its browser is not there to use or is paused while a person pays. What to do: Retry in a few seconds, or after the payment finishes when the browser is paused for one. - `cancel_failed` (500): The interaction was not withdrawn. What to do: Retry the call. - `connect_failed` (502): The connect link could not be made. What to do: Retry in a moment. - `connections_unavailable` (502): Connected apps could not be read. What to do: Retry in a moment. - `database_unavailable` (503): We could not reach our database. What to do: Retry in a few seconds. - `delete_failed` (502): The file could not be deleted. What to do: Try again. - `deployment_unavailable` (502): The deployment could not be read. What to do: Retry shortly. - `disconnect_failed` (502): The app could not be disconnected. What to do: Retry in a moment. - `download_unavailable` (502): A download link could not be made. What to do: Try again. - `generator_busy` (503): Drafting tests is slow right now. What to do: Retry after the `Retry-After` seconds. - `internal_error` (500): Something went wrong on our side. What to do: Retry. If it keeps happening, send Vestra the `request_id`. - `interview_failed` (502): The interview stopped before its draft was saved. What to do: Start again with a description. - `mcp_auth_required` (502): The MCP server asked for credentials it was not given. What to do: Give the server's credentials as a header with `${secret:NAME}`. - `mcp_failed` (500): The MCP request failed on our side. What to do: Retry it. - `mcp_missing_secret` (502): The MCP server names secrets that are not stored. What to do: Store them with `PUT /v1/secrets/{name}`. - `mcp_unreachable` (502): The MCP server did not answer. What to do: Check that it is up and reachable from the internet, then check it again. - `mcp_url_not_public` (502): The MCP server's address is not public. What to do: Use an https URL that reaches the public internet. - `memory_unavailable` (502): Memory could not be read or deleted. What to do: Retry shortly. - `memory_write_failed` (502): The memory could not be saved. What to do: Retry shortly. - `navigation_failed` (502): The page of a browser fetch could not be opened: the site did not answer, or the address was wrong. What to do: Check the address, then retry. - `not_implemented` (501): This is not available yet. What to do: Nothing to do now: the changelog says when it ships. - `queue_unavailable` (502): Routines are not taking runs right now. What to do: Try again in a minute. - `resolve_failed` (500): The answer was not recorded. What to do: Retry the call. - `return_unavailable` (502): Connect links are not available on this server. What to do: Contact Vestra support. - `route_failed` (502): A call an MCP tool made inside the API failed. What to do: Retry it. - `scenario_generation_failed` (502): Tests could not be drafted this time. What to do: Try again, or write the tests yourself in vestra.yaml. - `screenshot_failed` (502): The browser did not answer with a picture. What to do: Try again. - `timeout` (504): A browser fetch's page did not send every capture in time. `partial` holds what it did send. What to do: Use what `partial` holds, or retry with a longer `timeout_ms`, up to 20000. - `tool_result_undelivered` (502): The result could not reach the work. What to do: Try again. - `upload_failed` (502): The file could not be stored. What to do: Try again. - `user_create_failed` (502): The user could not be made. What to do: Retry the request. - `voice_connect_failed` (502): The call could not be opened. What to do: Start it again. - `voice_unavailable` (503): Voice is not available right now. What to do: Try again later. --- url: https://vestra.ai/developers/changelog.md --- # Changelog > What changed, and when. ## Browser fetch: a page's own data, with no model (October 2026) - `POST /v1/users/{user_id}/browser/fetch` opens one page in a background tab of the user's own browser, with its sign-ins, works your `steps` (`click`, `type`, `scroll`, `wait_for`, `page_fetch`) and answers with the bodies of the page's own data calls that match `capture`. No model runs. - Only sites in the deployment's `browser.allowed_domains` open (`403 domain_not_allowed`), and never a page on its `browser.blocked_paths`, such as a payment page (`403 page_blocked`, which leaves a `browser.page_blocked` row in `GET /v1/audit-log`: the host and path, never the query). - `keep_open_ms` keeps the tab, named by `tab`, for the next fetch, which sends no `url`. `front: true` opens it in front of the person's window instead, as a phone (its size, user agent and touch), for a sign-in they finish themselves; other fetches run behind it, a session's screen carries its `crop`, and neither the browser watch nor the screenshot (`409 browser_in_use`) ever shows its page. Four run at once (`409 browser_busy`), a person using the browser comes first (`409 browser_in_use`), and a slow page is `504 timeout` with what came in `partial`. - `GET /v1/users/{user_id}/browser/watch` streams the fetch's tabs live, read only; it never wakes the computer. A fetch's `tag` names its tab there. - An optional `click` takes `within_ms` (up to 10000): it looks that long for an element that shows a moment after the page, such as a site's popup, then goes on without it. - A `click` takes `nth` (which match, from 1) and `near_text` (the first match after a text, or after texts in turn). - `stage: true`: in front, one kept tab, alone in the watch. - `private: true`: a fetch the browser watch never shows. - `front_device: "desktop"`: a hand-off on the site's desktop page, as the other fetches see it. - `challenge: "turnstile"`: the page called Cloudflare's Turnstile check. - `GET /v1/users/{user_id}/browser/cookies?domain=&names=`: which cookies of those names the browser holds for an allowed site, and until when. Names and expiry only, never a value; it opens no page, so read it every few seconds to see a person finish signing in on a `front` page. - A session that only watches (`screen: "view"`) shows nothing while its screen is private: a person signs in on that computer (a `front` fetch, from its start until its tab closes), or a `private` fetch runs or keeps its tab (a later fetch on that tab counts too). `GET /v1/sessions/{session_id}` answers `screen_paused: true`, `GET /v1/sessions/{session_id}/screen` answers `409 browser_in_use`, and the screen page hangs up and says the screen is private right now, then comes back by itself. `GET /v1/users/{user_id}/screenshot` answers `409 browser_in_use` then too. A `control` session, the person's own, is unchanged. - `front_crop` (`role`, `name` or `text`): a hand-off's screen shows only that element of its page, such as a site's sign-in dialog, at a phone's size. From a `front` fetch's start until the element is on the page, `GET /v1/sessions/{session_id}/screen` answers `crop_pending: true` and the screen page shows no picture yet; after 20 seconds without it, the page's part shows as before. The screen page also asks again while the computer is still waking (`browser_not_open`), for up to 30 seconds. - A session's screen page checks every 3 seconds whether the session is still open. When your server ends it (`DELETE /v1/sessions/{session_id}`), the page says it can be closed, tells the page around it (`vestra:ended`; embed.js fires `vestra-ended`), tells a window that opened it the same, and closes its own window when a script opened it (a frame never closes itself). A screen opened before its hand-off takes the hand-off's `crop` as soon as it comes. - `POST /v1/sessions` takes `driver` with `screen: "control"`: `user` opens the screen in your user's hands from the first frame the page shows, with no Take control and nothing to give back, for a page that is theirs to use (a sign-in Bash opened for them). `bash`, or none, is as before: they watch first and tap Take control. `driver: "user"` with any other screen is `400`. - A `front_device: "desktop"` hand-off now runs at a phone's shape, 440 wide and as high as the window, still with the site's desktop page: a sign-in service's tab it opens draws into the same part of the screen, so a session's screen shows one rectangle whatever is in front. On a hand-off's page (a crop) the screen page has no bar: the person's first tap on the page takes control and opens the keyboard. ## What `approvals` enforces, said plainly (October 2026) - `approvals` lists what Bash asks the person before doing. The manifest and file references, the guides and the agent skill said it lists what "always waits for the person's yes", which only a system approval makes true, and not every path has one yet. A system approval enforces what `approvals` lists for your own functions and your MCP servers' tools that wait for a yes; in a browser or a connected app, Bash is instructed to ask. - Nothing changes in how Bash behaves: the docs now say what it does. When a system approval covers every path, this changelog says so. ## Bash's own names are reserved (October 2026) `/v1` is not public yet, so this changes the contract on purpose. - A function tool named like one of Bash's own tools (`execute_code`, `mcp_invoke`, `github_pull_request` and the rest of the box's catalog) is refused with `400 tool_name_reserved` when a version is saved (a file or a manifest) or a message brings it. Bash skipped such a function before, keeping its own tool, so the call you meant never happened. The check is exact: only Bash's own names, never a pattern. The names are reserved whether or not a deployment uses that capability: `remember` is refused even for a company whose memory is off, and a tool behind a feature not yet on for you is reserved all the same, so turning it on never breaks a saved version. - An MCP server's slug can't be one of Bash's own tool servers (`composio`, `browser-control`, `os-hands`, `vestra`): `POST /v1/mcp-servers` refuses it with `400 tool_name_reserved` on `slug`, a file never registers one, and a new version naming a server made before this (directly, or by its URL in a file) is refused the same way. Register the server again under another slug and point your next version at it (a file then uses the new one); don't delete the old one while a live version still runs it. - Nothing already saved changes: a version saved before this keeps running, Bash's own tool answering to the name. Only a new version, a new server or a new message is checked. ## A limit's stop is a stop (October 2026) `/v1` is not public yet, so this changes the contract on purpose. - Work that a limit or a policy stopped now ends with `task.stopped`, as the conversation's own record already said, and says why in two new fields: `reason`, one of `max_turns` (it used every turn it had), `max_budget` (it reached a budget cap), `credits_exhausted` (the account's credits ran out) or `hook_blocked` (a policy hook in your company's rules stopped it), and `message`, more about why in plain words (for `hook_blocked`, the hook's own words; `null` when there is no more). The first two used to arrive as `task.failed` with that `reason` and `message: null`. - Credits running out has its own reason, `credits_exhausted`, on `task.stopped`, whether the account was out of credits before the work started or ran out while it ran. It used to arrive as `task.failed` with `max_budget` (or `credits_exhausted`), or as a stop with no reason; `max_budget` now means only a budget cap (a task's limit, a turn's spend ceiling, or the day's budget). - A stop by you or a person carries neither field, exactly as before. Wait on `task.stopped` or `task.failed` as you already do: only the event a limit's stop arrives as has moved. - Both SDKs type the two fields on `task.stopped` (optional), and the events reference lists them. The CLI's event lines and `init`'s interview say why a stopped work stopped when the event says. ## The one yes shows the price (October 2026) - `vestra init` now gives a function that waits for your customer's yes each value the customer agrees to as an input, so its approval shows them: a bike rental's `extend_rental` takes `rental_id`, `charge_usd` and `new_due_back`. With `rental_id` alone, its approval read "charges its daily_price_usd" with no number. Init asks what the customer agrees to and where Bash finds it, declares those values as required inputs named and written like your example results, and writes instructions that say where each comes from ("call extend_rental with charge_usd set to daily_price_usd from get_rental"). When no lookup returns a value (your code works the price out as it acts), init asks for one that does, such as a quote, and declares it `read_only`: Bash never guesses a value for the approval. - Your code checks those values against its own before it acts, and throws when one differs, so nothing is charged that the customer did not see. Init's closing words say so for each such function, naming its inputs, and for one whose approval shows only what it acts on, they say to add the values the customer should see. A function you declared before keeps its inputs: add the values yourself, and the check in your code. - A company action's `summary` (at most 120 characters) never cuts an argument any more. Short of room, it leaves out the description's first sentence, then each argument that does not fit, and ends `and N more`: `extend_rental: rental_id R-1042, charge_usd 15, new_due_back 2026-10-05 18:00` where it used to read `... extend_rental: rental_id R-1042, charge_us…`. `details.rows` is unchanged: the call's first 8 arguments, each value up to 200 characters. - The tests `vestra init` and `vestra test --generate` draft check what the approval shows (`The approval shows the charge of 12 and the new return time 2026-10-07 17:30.`), never what Bash says before it: Bash may go from its lookup straight to the approval. The starter test of the customer's no is kept once drafted, whatever a check of its wording finds. ## A test never makes a routine (October 2026) - In a test run, a routine the test customer asks for is never made: Bash answers as it would for a real one, with when it would run, but nothing is scheduled and nothing runs later. Asked for their routines, the test customer sees only the ones made in that test, never another test's or an earlier run's. Your real customers' routines are unchanged. 10 older changes are on the changelog's own page: https://vestra.ai/developers/changelog.md --- url: https://vestra.ai/developers/reference/sdk.md --- # SDKs and CLI reference > Every method of the TypeScript and Python SDKs, and every command of the CLI. ## TypeScript `import { Vestra } from '@vestra/sdk'`, then `new Vestra()`: each line of the card is a method, and `vestra.user(id)` acts for one of your users. ```bash npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=module ``` ### The client - `new Vestra(opts?: VestraOptions): Vestra`: Your company's door to Bash: your key from VESTRA_API_KEY, the base URL from VESTRA_BASE_URL, and your tools. - `vestra.user(id: string): UserBash`: One of your users' Bash, by your own id for them. They are created the first time you act for them. ### `vestra.init` (the [`init`](https://vestra.ai/developers/init.md) line) - `vestra.init(opts: InitOptions): Draft`: Bash interviews you and drafts vestra.yaml: iterate draft.questions(), then await draft.yaml(). - `vestra.init.closing(deployment: string): Promise`: The closing words of an interview run elsewhere (over MCP, say), by its deployment: as Draft.closing() gives them. ### `vestra.deploy` (the [`deploy`](https://vestra.ai/developers/deploy.md) line) - `vestra.deploy(path?: string, opts?: DeployOptions): Promise`: Apply vestra.yaml: a new version, promoted in the key's mode once its tests pass; .rollback(name). - `vestra.deploy.rollback(name: string, opts?: { version?: number; }): Promise<…>`: Put an earlier version back live, at once: the one before, or { version }. Needs a live key. ### `vestra.test` (the [`test`](https://vestra.ai/developers/test.md) line) - `vestra.test(target?: string, opts?: TestOptions): Promise`: Run vestra.yaml's tests and wait for the verdict: an edited file is applied first as a version of its own, not promoted (a live key applies nothing), and the summary's note says so; .generate(name) drafts more. - `vestra.test.wait(runId: string, opts?: { interval_ms?: number; }): Promise`: A run already started, followed to its verdict: the one a deploy started (its tests.id), which promotes when it passes, so the summary says what it promoted. - `vestra.test.generate(target?: string, opts?: { count?: number; version?: number; }): Promise<…>`: Scenarios drafted from the deployment's instructions, as vestra.yaml's tests: entries; from `version`, or else the version test mode runs. ### `vestra.ask` (the [`ask`](https://vestra.ai/developers/ask.md) line) - `vestra.ask(message: string, opts?: AskOptions): Run`: A job in plain words. Returns a Run at once: iterate its events, stop() it, read text(), output() and cost(). - `vestra.ask(items: BatchInput[], opts?: BatchOptions): Batch`: Many users at once: a batch, one conversation each. ### `vestra.routines` (the [`routines`](https://vestra.ai/developers/routines.md) line) - `vestra.routines.create(input: RoutineInput): Promise<…>`: A job Bash runs on a schedule in plain words (every), as cron (schedule), or whenever your system calls (on: 'call'). - `vestra.routines.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise>`: A user's routines (or your company's own Bash's), with when each runs next. - `vestra.routines.get(id: string): Promise<…>`: One routine: what it asks, when it runs next, and when it last ran. - `vestra.routines.update(id: string, patch: Partial>): Promise<…>`: Change what it asks, when it runs, its filters or its name. - `vestra.routines.pause(id: string): Promise<…>`: Stop its runs, and keep it. - `vestra.routines.resume(id: string): Promise<…>`: Start its runs again. - `vestra.routines.delete(id: string): Promise<{ id: string; object: "routine"; deleted: true; }>`: Delete it: it never runs again. - `vestra.routines.run(id: string): Promise<{ … }>`: Run it once, now. - `vestra.routines.trigger(id: string, payload?: Record): Promise<{ … }>`: Fire a routine that runs on a call, with your system's payload. - `vestra.routines.runs(id: string, q?: { cursor?: string; limit?: number; }): Promise>`: Its runs, newest first, each with the conversation it ran in. ### `vestra.answer` (the [`answer`](https://vestra.ai/developers/answer.md) line) - `vestra.answer(id: string, body: { … } | { answers: string[]; } | { done: true; } | { could_not: true; } | { not_now: true; } | { output: unknown; } | { error: string; }): Promise<{ … }>`: Answer what Bash waits on, by its id, with the body its type takes: approve, answers, done, or your tool's output. - `vestra.answer.list(q?: InteractionQuery): Promise>`: What Bash waits on: every user's, or one user's with `user`. - `vestra.answer.get(id: string): Promise<…>`: One interaction: what Bash waits on, its questions, its nonce, and how it settled. - `vestra.answer.cancel(id: string, reason?: string): Promise<…>`: Withdraw it: the task carries on without that action. ### `vestra.watch` (the [`watch`](https://vestra.ai/developers/watch.md) line) - `vestra.watch(opts?: { control?: boolean; conversation?: string; }): Promise`: Its screen, live: watch only, or { control: true } so a person can take the wheel. ### `vestra.talk` (the [`talk`](https://vestra.ai/developers/talk.md) line) - `vestra.talk(opts?: { conversation?: string; voice?: string; }): Promise`: A voice call with Bash: open session.url and the page joins the call. ### `vestra.events` (the [`events`](https://vestra.ai/developers/events.md) line) - `vestra.events(id: string, opts?: { after?: number; signal?: AbortSignal; }): AsyncGenerator`: A conversation's events (conv_…), or one task's (task_…), as they happen: its kept rows first, then live. Stop by breaking out. - `vestra.events.trace(task: string): Promise<…>`: Every step of a task, afterwards, with tokens and cost per turn. - `vestra.events.to(url: string, opts?: { events?: string[]; description?: string; }): Promise<{ … }>`: Send events to your server, signed. The signing secret is in the answer, shown once. - `vestra.events.list(q?: EventLogQuery): Promise<{ data: VestraEvent[]; next_cursor: string | null; }>`: The record: the events your webhooks get, newest first. - `vestra.events.tail(q?: TailQuery): AsyncGenerator`: The record from now on, oldest first, read every interval_ms. - `vestra.events.handler(fn: (event: VestraEvent) => unknown, opts?: HandlerOptions): WebhookHandler`: A webhook handler: checks the signature, answers your tools' calls itself, and hands every other event to fn. - `vestra.events.verify(payload: string | Uint8Array, header: string | null | undefined, secret?: string): Promise`: One webhook checked by hand, from its raw body and its Vestra-Signature header. ### `vestra.files` (the [`files`](https://vestra.ai/developers/files.md) line) - `vestra.files.list(q?: FileQuery): Promise>`: A user's files (or your company's own Bash's), one task's outputs, or one conversation's. - `vestra.files.get(id: string): Promise<{ … }>`: Its details, with a download link good for a short while. - `vestra.files.download(id: string): Promise>`: Its bytes. - `vestra.files.upload(file: Upload, opts?: { user?: string; filename?: string; type?: string; }): Promise<{ … }>`: Hand Bash a file (10 MB at most) for this user, or for your company's own Bash. - `vestra.files.delete(id: string): Promise<{ id: string; object: "file"; deleted: true; }>`: Delete a file. ### `vestra.connect` (the [`connect`](https://vestra.ai/developers/connect.md) line) - `vestra.connect(app: string, opts?: { … }): Promise<…>`: A sign-in link for one app: send it to your user, or open it yourself for your company's own Bash. - `vestra.connect.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise>`: The apps a user has connected (or your company's own Bash has), with each one's status. - `vestra.connect.get(id: string, opts?: { user?: string; }): Promise<{ … }>`: One connected app and its status. - `vestra.connect.remove(id: string, opts?: { user?: string; }): Promise<{ object: "connection"; id: string; deleted: true; }>`: Disconnect the app: Bash can no longer use it for this user. - `vestra.connect.catalog(q?: { … }): Promise>`: Every app your users may connect, after your allowlist. ### `vestra.memory` (the [`memory`](https://vestra.ai/developers/memory.md) line) - `vestra.memory.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise<…>`: A written profile, and each memory, newest first. - `vestra.memory.search(q: string, opts?: { user?: string; }): Promise<…>`: Ask what Bash remembers: { found, text }. - `vestra.memory.add(text: string, opts?: { user?: string; kind?: MemoryKind; }): Promise<{ … }>`: Tell Bash a fact once. - `vestra.memory.forget(id: string, opts?: { user?: string; }): Promise<{ id: string; deleted: true; }>`: Forget one memory. - `vestra.memory.forgetAll(opts?: { user?: string; }): Promise<{ … }>`: Forget everything about this user, or your company's own Bash. ### `vestra.embed` (the [`embed`](https://vestra.ai/developers/embed.md) line) - `vestra.embed(opts?: { … }): Promise`: Bash inside your app: chat, approvals, the live screen and voice. memory: true adds the memory panel (PATCH /v1/org { memory_panel: true } first). ### `vestra.users` (the [`users`](https://vestra.ai/developers/users.md) line) - `vestra.users.list(q?: { cursor?: string; limit?: number; }): Promise>`: Your users, with this month's spend. - `vestra.users.create(user: string, profile?: UserProfile): Promise<…>`: Set a user up before first use (a name, a default deployment, a cap). Safe to repeat. ### `vestra.usage` (the [`usage`](https://vestra.ai/developers/usage.md) line) - `vestra.usage(q?: UsageQuery): Promise<…>`: What was spent between two dates, grouped by day, user, deployment, conversation, task, model or category. - `vestra.usage.lineItems(q: { … }): Promise>`: The receipt: the charges behind one conversation or one task. - `vestra.usage.caps(patch: { … }): Promise<…>`: Set the day and month caps and the alert thresholds. - `vestra.usage.limits(): Promise<…>`: The caps and alert thresholds, with what is spent against each. ### `vestra.keys` (the [`keys`](https://vestra.ai/developers/keys.md) line) - `vestra.keys.list(): Promise>`: Your keys: names, modes, scopes and the last four characters, never the secret. - `vestra.keys.create(opts?: { … }): Promise<{ … }>`: A new key in this key's mode (a test key makes test keys). Its secret is shown once. - `vestra.keys.roll(id: string, opts?: { grace_seconds?: number; }): Promise<…>`: Replace a key; the old one keeps working for grace_seconds (a day by default). - `vestra.keys.revoke(id: string): Promise<{ … }>`: Revoke a key: it stops working at once. - `vestra.keys.me(): Promise<…>`: Your company, and the key in use. ### `vestra.secrets` (the [`secrets`](https://vestra.ai/developers/secrets.md) line) - `vestra.secrets.set(name: string, value: string, opts?: { user?: string; }): Promise<{ … }>`: Store it, write-only: your company's, or one user's own. Storing it again replaces it. - `vestra.secrets.list(q?: { user?: string; cursor?: string; limit?: number; }): Promise>`: Names only, never values. - `vestra.secrets.delete(name: string, opts?: { user?: string; }): Promise<{ … }>`: Delete a secret: your company's, or one user's own. ### `vestra.sessions` - `vestra.sessions.get(id: string): Promise<{ … }>`: A session's state: open or ended, and what it shows. - `vestra.sessions.end(id: string): Promise<{ … }>`: End a session now: its token stops working at once. ### `vestra.raw` - `vestra.raw.GET

(path: P, init?: RawInit>): Promise>>>`: Read any route by its path, typed from the contract: { data, error, response }. - `vestra.raw.POST

(path: P, init?: RawInit>): Promise>>>`: Write to any route by its path, typed from the contract: { data, error, response }. - `vestra.raw.PUT

(path: P, init?: RawInit>): Promise>>>`: Put to any route by its path, typed from the contract: { data, error, response }. - `vestra.raw.PATCH

(path: P, init?: RawInit>): Promise>>>`: Change through any route by its path, typed from the contract: { data, error, response }. - `vestra.raw.DELETE

(path: P, init?: RawInit>): Promise>>>`: Delete through any route by its path, typed from the contract: { data, error, response }. ### `vestra.user(id)` - `vestra.user(id).id: string` - `vestra.user(id).get(): Promise<…>`: Their profile, their spend this month and what they may still spend. - `vestra.user(id).update(patch: { name?: string | null; email?: string | null; locale?: string | null; timezone?: string | null; metadata?: { [key: string]: unknown; }; spend_cap_usd?: number | null; monthly_cap_usd?: number | null; default_deployment?: string | null; status?: "active" | "disabled"; }): Promise<…>`: Change their name, email, locale, time zone, default deployment, cap or metadata, or switch them off. - `vestra.user(id).export(): Promise<{ … }>`: Everything held for them, as one download link (a data-rights request). - `vestra.user(id).delete(): Promise<{ … }>`: Erase their Bash, computer, memory and files. It runs as a job; user.deleted says when it is done. - `vestra.user(id).warm(): Promise<{ user_id: string; status: "ready" | "waking"; }>`: Wake their computer now, so their next task starts at once. - `vestra.user(id).screenshot(): Promise>`: What their computer's screen shows now, as a JPEG. ### `run` An ask, as it happens: iterate it with for await to read each event, and answer what Bash waits on as it comes. - `run.conversation: string | null`: The conversation (conv_…), once the server has named it. - `run.status: RunStatus | null`: How it ended: completed, failed or stopped. Null while it runs, or when it failed before it started. - `run.stop(): Promise<{ … } | null>`: Stop the reply and every piece of work the conversation started. - `run.text(): Promise`: The answer, once the run has ended, trimmed: when Bash went to work, the work's answer, its final reply (task.completed's answer; from an older server without one, its summary, which joins every step's words); otherwise, and when the work ended with no final reply, the reply's answer, its final words (message.completed's answer; from an older server without one, the whole reply). Throws the turn's error when the turn failed (as output() does), so a failed turn never reads as an empty answer. - `run.output(): Promise`: The result you asked for with `output`. Throws the turn's error when the turn failed or the answer did not fit it. - `run.cost(): Promise`: What this run cost, read once it has ended. - `run.ask(message: string, opts?: Omit): Run`: Carry on in the same conversation, once this run has ended, so the next message never meets a turn still running. - `run.unsentAnswers(ms?: number): Promise`: Waits up to `ms` for the answers this run is still sending to your tools' calls (each function's result, then the write that carries it), and returns the calls whose answers had not landed by then: Bash may still be waiting on those, so answer them with vestra.answer. The run never waits on its own answers, so await this before your process exits, on every way out, an error or an abort included. ### `batch` Many users at once, one conversation each: follow it with wait(), and read each item's result with items(). - `batch.created: Promise<…>`: The batch as the server answered its creation. - `batch.id: string | null`: bat_…, once the batch exists. - `batch.get(): Promise<…>`: The batch now: its status and how many items are queued, running, done or failed. - `batch.items(): AsyncGenerator<…>`: Every item, with its conversation, task and result, page by page. - `batch.cancel(): Promise<…>`: Start no new item; the running ones finish. - `batch.wait(opts?: { interval_ms?: number; signal?: AbortSignal; }): Promise<…>`: Until the batch is no longer running; each read rides out a lost connection (a redeploy) for up to 5 minutes. ### `session` A session opened for a browser: send its url to the page, and end it when you are done. - `session.id: string` - `session.url: string`: Open this in a tab or an iframe: chat, approvals, the screen and voice, as the session was opened. - `session.token: string`: The session's own token: it reaches this user (and this conversation) only, and never money. - `session.expires_at: string` - `session.user_id: string | null` - `session.conversation_id: string | null` - `session.view: { … }`: The whole answer of POST /v1/sessions. - `session.end(): Promise<{ … }>`: End it now: its token stops working at once, and its streams and calls close. - `session.take(): Promise<…>`: Take the wheel (a session opened with control). - `session.giveBack(): Promise<…>`: Give the wheel back to Bash. - `session.screen(): Promise<…>`: The screen's stream credentials, made per call, good for 5 minutes. ### `draft` vestra.init(): Bash interviews you (POST /v1/deployments/interview) and drafts vestra.yaml. - `draft.started: Promise<…>` - `draft.questions(opts?: { timeout_ms?: number; }): AsyncGenerator`: Bash's questions, one reply at a time. Answer each to go on; it ends once the draft is saved, or at a question left unanswered. An interview that says nothing for timeout_ms (INTERVIEW_WAIT_MS) ends with interview_failed. - `draft.isDrafted(): Promise`: Whether the interview has saved its draft (the draft moved past version 1). - `draft.yaml(opts?: { interval_ms?: number; timeout_ms?: number; }): Promise`: vestra.yaml, as the interview drafted it (?format=yaml), once the draft is saved. - `draft.closing(): Promise`: The interview's closing words, once its draft is saved: what the draft sets Bash up to do, the tools your code answers, the spending cap per task, the starter tests, and the next steps. Show them where init ends. Null when the draft version carries none. ### `http` The one door every call goes through: your key, the base URL, retries and errors. vestra.raw uses it too. - `http.apiKey: string` - `http.baseUrl: string` - `http.fetch(input: RequestInfo | URL, init?: RequestInit): Promise`: The fetch every call goes through: yours from the options, or the global one. - `http.fetch(input: string | Request | URL, init?: RequestInit): Promise`: The fetch every call goes through: yours from the options, or the global one. - `http.mode: "live" | "test"`: The key's mode, from its prefix: a test key sees test users, test deployments and test money. - `http.url(path: string, query?: Query): string`: A route's whole URL, with its query. - `http.json(method: string, path: string, opts?: CallOptions): Promise`: A call whose answer is JSON, or nothing for a 204. - `http.readThrough(path: string, opts?: CallOptions): Promise`: A GET whose answer is JSON, read on through a lost connection (Reconnects): what a wait polls. - `http.writeThrough(method: string, path: string, opts?: CallOptions): Promise`: A write whose answer is JSON, sent under one Idempotency-Key and sent again under it through a lost connection (Reconnects): a 503 while the server checks your key never ends it, and the server takes the write once. - `http.send(method: string, path: string, opts?: CallOptions, accept?: string): Promise`: A call whose answer the caller reads (a stream of events, or a file). Throws VestraError unless it is 2xx. - `http.request(method: string, path: string, opts?: CallOptions, accept?: string): Promise`: One call as every call is made (vestra.raw's too), with the key, the user agent and the tries above: its answer as it came, 2xx or not. ### Functions - `errorOf(res: Response): Promise`: A non-2xx answer as a VestraError, from its /v1 error body when it has one. - `lostConnection(err: unknown): string | null`: Why a failed call is the connection lost: a redeploy's 502, 503 or 504, a transport failure, or a first try that died mid-call and left its key in progress (409 idempotency_in_progress: the server says retry in a moment, with the same key). Null for an answer to act on, an abort, or a bug. - `eventFromStream(row: StreamRow, where: { user: string | null; conversation_id: string | null; }): RawEvent | null`: A stream row, read for one user's conversation, as the one shape. - `eventFromWebhook(body: WebhookBody): RawEvent | null`: A webhook's body (or an item of GET /v1/events) as the one shape. - `runTool(fn: ToolFunction, args: unknown, call: ToolCall): Promise`: Your function's answer as the answer route takes it: { output } or { error }. - `tool(def: { … }): Tool`: A tool your code answers: its name, what it does, its input in plain words or JSON Schema, and the function that runs it. It waits for your user's yes unless you mark it read_only (a lookup) or run_without_asking. - `toJsonSchema(shape: Record): JsonSchema`: A shape in plain words, as vestra.yaml's tools write their input, as JSON Schema: each field's type is string, number, integer, boolean or object, with [] for a list and ? for optional, and a nested map for a nested object: { pnr: 'string', seats: 'integer?', legs: 'string[]', trip: { from: 'string' } }. An input with type: 'object', properties or $schema is JSON Schema and is kept as it is. - `nameFrom(describe: string): string`: A deployment name from the start of a description: lowercase words and dashes, at most 63. - `plainClosing(c: C): C`: The closing words in plain words: the model wrote the summary and the tool descriptions, and the next steps name them. The server cleans them where it writes them; this is the second layer, where the developer reads them. - `testRunOutcome(run: { … } & RunPromotion): string`: A test run's status in plain words: an error says why, from its error_code, and a run a restart interrupted says so: resumed, or interrupted once too often. - `skillBundleSha256(files: readonly BundleFile[]): string`: sha256 over each file's path, its byte length and its bytes (UTF-8), in path order: the server's Skill.sha256. - `parseSseFrames(buffer: string): { frames: VestraStreamEvent[]; rest: string; }`: Split an SSE buffer into complete frames and the unfinished rest. Comment lines are skipped. - `streamEvents(opts: StreamOptions): AsyncGenerator`: Follow a /v1 event stream. A dropped or closed connection, or one with nothing for 45 s, not even the server's 15 s ping, reconnects with Last-Event-ID, so nothing is missed and nothing repeats: through a redeploy too, for up to 5 minutes without a new event (RECONNECT), then connection_lost. Stop by breaking out of the loop or aborting the signal. A 410 means the resume point is gone (older than retention, or from another stream): read the resource, start over. - `signPayload(secret: string, timestampSec: number, payload: string): string`: Hex HMAC-SHA256 of `${timestampSec}.${payload}` with the endpoint's signing secret. - `verifyWebhook(input: { … }): unknown`: Check a webhook before trusting it. `payload` is the raw request body, exactly as received (never re-serialized JSON), as text or as bytes (read as UTF-8). Returns the parsed body. ### Reconnects - `new Reconnects(clock?: { … }): Reconnects`: One lost connection after another: `ok()` when it is back, `lost(why)` waits before the next try, or throws connection_lost once the budget is spent. - `Reconnects.ok(): void`: The connection is back (a new event, or an answer): the next loss starts a fresh 5 minutes, and short waits. - `Reconnects.opened(): void`: A 2xx connection stayed up past RECONNECT.heldOpenMs and said something: the next loss starts a fresh 5 minutes; the waits keep growing until a frame comes. One that answers and closes at once, or never says a word, does not count, so a server that only does that is given up on. - `Reconnects.lost(why: string, signal?: AbortSignal): Promise`: Lost again: wait before the next try (1 s doubling to 15 s, with jitter), or throw connection_lost once 5 minutes have passed. ### VestraError - `new VestraError(status: number, error?: ErrorBody): VestraError`: The /v1 error body, { error: { type, code, message, param?, request_id } }, as an exception. - `VestraError.status: number` - `VestraError.type: string` - `VestraError.code: string` - `VestraError.param: string | undefined` - `VestraError.requestId: string | undefined` - `VestraError.names: string[] | undefined`: secret_missing: the secrets to store before the call can succeed. - `VestraError.deployments: string[] | undefined`: mcp_server_shared: the other deployments that run the MCP server the file would change. ### StreamExpiredError - `new StreamExpiredError(message?: string): StreamExpiredError`: The events after your resume point are no longer kept: read the conversation again, and follow it from now. - `new StreamExpiredError(message?: string, options?: ErrorOptions): StreamExpiredError`: The events after your resume point are no longer kept: read the conversation again, and follow it from now. - `StreamExpiredError.code: "stream_expired"` ### StreamHttpError - `new StreamHttpError(status: number): StreamHttpError`: The stream answered an error that retrying cannot fix: a bad key, no access, no such stream, or a 429 that outlasted its waits. - `StreamHttpError.status: number` ### WebhookVerificationError - `new WebhookVerificationError(code: "missing_header" | "bad_header" | "too_old" | "bad_signature", message: string): WebhookVerificationError`: A webhook failed its check: no signature header, a bad one, too old, or a signature that does not match. - `WebhookVerificationError.code: "missing_header" | "bad_header" | "too_old" | "bad_signature"` ### Constants - `DEFAULT_BASE_URL: "https://api.vestra.ai"`: The API's address when neither baseUrl nor VESTRA_BASE_URL is given. - `RECONNECT: { … }`: How a reader rides out a lost connection (a redeploy answers 502 for a minute or two, or refuses the connection): waits that double from 1 s to at most 15 s, each with jitter, for up to 5 minutes in a row. Past that, it gives up with connection_lost. - `SDK_VERSION: "0.1.8"`: This SDK's version, sent in its user agent. - `EVENT_DELIVERY: { … }`: Where each type is delivered: the stream carries what happens in a conversation, webhooks the durable events. - `EVENT_TYPES: readonly ["message.delta", "message.completed", "task.started", "task.progress", "task.completed", "task.failed", "task.stopped", "interaction.created", "interaction.resolved", "interaction.expired", "screen.control", "session.started", "session.ended", "file.created", "routine.fired", "usage.updated", "usage.threshold_reached", "usage.cap_reached", "conversation.status", "user.created", "user.deleted", "connection.completed", "connection.failed", "connection.removed", "memory.observed", "batch.completed", "deployment.version_created", "deployment.promoted", "deployment.rolled_back", "test_run.completed", "key.created", "key.revoked", "error", "ping"]` - `INTERVIEW_WAIT_MS: 600000`: How long the interview may say nothing before init gives up (questions()), and how long yaml() waits for the draft. - `SIGNATURE_HEADER: "vestra-signature"` - `CONTRACT_TYPES_RESOLVE: true`: Compile-time proof that every name above is a real route's answer: a moved route or a wrong status makes one `never`, and this line stops compiling. ## Python `from vestra import Vestra`, then `Vestra()`: it blocks, and each call returns its answer. `AsyncVestra` has the same names, each awaited. ```bash pip install https://vestra.ai/developers/kit/vestra-0.1.8-py3-none-any.whl ``` ### The client - `Vestra(api_key: Optional[str] = None, base_url: Optional[str] = None, tools: Optional[Tools] = None, transport: Optional[Transport] = None, max_retries: int = 2)`: api_key defaults to VESTRA_API_KEY, base_url to VESTRA_BASE_URL. tools answers Bash's calls to your tools. - `vestra.close() -> None`: End the client: the work still on its way is cancelled, its connections closed, and its loop stopped. - `AsyncVestra(api_key: Optional[str] = None, base_url: Optional[str] = None, tools: Optional[Tools] = None, transport: Optional[Transport] = None, max_retries: int = 2)`: Your company's door to Bash, awaited: the same lines as Vestra. api_key defaults to VESTRA_API_KEY, base_url to VESTRA_BASE_URL. - `vestra.aclose() -> None`: Close the client's connections. `async with AsyncVestra() as vestra:` closes them for you. - `vestra.user(user_id: str) -> AsyncUserBash`: One of your users' Bash, by your own id for them. ### `vestra.init` (the [`init`](https://vestra.ai/developers/init.md) line) - `vestra.init(describe: str, for_: str = 'customers', name: Optional[str] = None) -> AsyncDraft`: Bash interviews you and drafts vestra.yaml: iterate draft.questions(), then await draft.yaml(). ### `vestra.deploy` (the [`deploy`](https://vestra.ai/developers/deploy.md) line) - `vestra.deploy(path: str = 'vestra.yaml', live: bool = False, promote: str = 'after_tests', shared: str = 'write') -> Data`: Apply vestra.yaml: a new version, promoted in the key's mode once its tests pass (promote='now' or 'no' to choose). - `vestra.deploy.rollback(name: str, version: Optional[int] = None) -> Data`: Put an earlier version back live, at once: the one before, or version=. Needs a live key. ### `vestra.test` (the [`test`](https://vestra.ai/developers/test.md) line) - `vestra.test(target: str = 'vestra.yaml', version: Optional[int] = None, user_secrets: Optional[Mapping[str, str]] = None, interval: float = 3.0) -> Data`: Run the tests and wait for the verdict: an edited vestra.yaml is applied first as a version of its own, not promoted (a live key applies nothing), and the summary's note says so. - `vestra.test.generate(target: str = 'vestra.yaml', count: Optional[int] = None, version: Optional[int] = None) -> List[Data]`: Scenarios drafted from the deployment's instructions, as vestra.yaml's tests: entries; from `version`, or else the version test mode runs. - `vestra.test.wait(run_id: str, interval: float = 3.0) -> Data`: A run already started, followed to its verdict: the one a deploy started (its tests.id), which promotes when it passes, so the summary says what it promoted. ### `vestra.ask` (the [`ask`](https://vestra.ai/developers/ask.md) line) - `vestra.ask(what: Union[str, Sequence[Mapping[str, Any]]], **opts: Any) -> Union[AsyncRun, AsyncBatch]`: A job in plain words (an AsyncRun; user= names whose Bash), or a list of {"user", "message"} for many users at once (an AsyncBatch). ### `vestra.routines` (the [`routines`](https://vestra.ai/developers/routines.md) line) - `vestra.routines.create(ask: str, *, every: Optional[str] = None, timezone: Optional[str] = None, schedule: Optional[Mapping[str, str]] = None, on: Optional[str] = None, filters: Optional[Mapping[str, Any]] = None, name: Optional[str] = None, conversation: Optional[str] = None, deployment: Optional[str] = None, enabled: Optional[bool] = None, user: Optional[str] = None) -> Data`: every: "weekday at 08:00", "mon, wed, fri at 18:30", "30 minutes". on="call": it runs whenever your system posts to its trigger_url. - `vestra.routines.delete(routine_id: str) -> Data`: Delete it: it never runs again. - `vestra.routines.get(routine_id: str) -> Data`: One routine: what it asks, when it runs next, and when it last ran. - `vestra.routines.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: A user's routines (or your company's own Bash's), with when each runs next. - `vestra.routines.pause(routine_id: str) -> Data`: Stop its runs, and keep it. - `vestra.routines.resume(routine_id: str) -> Data`: Start its runs again. - `vestra.routines.run(routine_id: str) -> Data`: Run it once, now. - `vestra.routines.runs(routine_id: str, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Its runs, newest first, each with the conversation it ran in. - `vestra.routines.trigger(routine_id: str, payload: Optional[Mapping[str, Any]] = None) -> Data`: Fire a routine that runs on a call, with your system's payload. - `vestra.routines.update(routine_id: str, **patch: Any) -> Data`: Change what it asks, when it runs, its filters or its name. ### `vestra.answer` (the [`answer`](https://vestra.ai/developers/answer.md) line) - `vestra.answer(interaction_id: str, **body: Any) -> Data`: Answer what Bash waits on, by its id, with the body its type takes: approve=, answers=, done=, or your tool's output=. - `vestra.answer.cancel(interaction_id: str, reason: Optional[str] = None) -> Data`: Withdraw it: the task carries on without that action. - `vestra.answer.get(interaction_id: str) -> Data`: One interaction: what Bash waits on, its questions, its nonce, and how it settled. - `vestra.answer.list(user: Optional[str] = None, conversation: Optional[str] = None, task: Optional[str] = None, type: Optional[str] = None, status: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: What Bash waits on (pending, unless you ask for another status): every user's, or one user's. ### `vestra.watch` (the [`watch`](https://vestra.ai/developers/watch.md) line) - `vestra.watch(control: bool = False, conversation: Optional[str] = None) -> LiveSession`: Its screen, live: watch only, or control=True so a person can take the wheel. ### `vestra.talk` (the [`talk`](https://vestra.ai/developers/talk.md) line) - `vestra.talk(conversation: Optional[str] = None, voice: Optional[str] = None) -> LiveSession`: A voice call with Bash: open session.url and the page joins the call. ### `vestra.events` (the [`events`](https://vestra.ai/developers/events.md) line) - `vestra.events(id: str, after: int = 0) -> AsyncIterator[Event]`: A conversation's events (conv_...) or one task's (task_...), its kept rows first, then live. Stop by breaking out. - `vestra.events.handler(fn: Callable[[Event], Any], secret: Optional[str] = None, tolerance: int = 300) -> 'WebhookHandler'`: A webhook handler: checks the signature, answers your tools' calls itself, and hands every other event to fn. - `vestra.events.list(type: Optional[str] = None, user: Optional[str] = None, task: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: The record: the events your webhooks get, newest first, as the one shape (lists leave `interaction` None: read it with answer.get). - `vestra.events.tail(type: Optional[str] = None, user: Optional[str] = None, after: Optional[str] = None, interval: float = 2.0) -> AsyncIterator[Event]`: The record from now on (or after an evt_ id), oldest first, read every `interval` seconds. Stop by breaking out. - `vestra.events.to(url: str, events: Optional[Sequence[str]] = None, description: Optional[str] = None) -> Data`: Send events to your server, signed. The signing secret is in the answer, shown once. - `vestra.events.trace(task_id: str) -> Data`: Every step of a task, afterwards, with tokens and cost per turn. - `vestra.events.verify(payload: Union[str, bytes], header: Optional[str], secret: Optional[str] = None) -> Optional[Event]`: One webhook checked by hand, from its raw body and its Vestra-Signature header. ### `vestra.files` (the [`files`](https://vestra.ai/developers/files.md) line) - `vestra.files.delete(file_id: str) -> Data`: Delete a file. - `vestra.files.download(file_id: str) -> bytes`: Its bytes. - `vestra.files.get(file_id: str) -> Data`: Its details, with a download link good for a short while. - `vestra.files.list(user: Optional[str] = None, conversation: Optional[str] = None, task: Optional[str] = None, source: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: A user's files (or your company's own Bash's), one task's outputs, or one conversation's. - `vestra.files.upload(file: Any, filename: Optional[str] = None, content_type: Optional[str] = None, user: Optional[str] = None) -> Data`: Hand Bash a file (10 MB at most): bytes, a path, or an open file. ### `vestra.connect` (the [`connect`](https://vestra.ai/developers/connect.md) line) - `vestra.connect(app: str, user: Optional[str] = None, return_url: Optional[str] = None, deployment: Optional[str] = None) -> Data`: A sign-in link for one app: send it to your user, or open it yourself for your company's own Bash. - `vestra.connect.catalog(search: Optional[str] = None, category: Optional[str] = None, deployment: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Every app your users may connect, after your allowlist. - `vestra.connect.get(connection_id: str, user: Optional[str] = None) -> Data`: One connected app and its status. - `vestra.connect.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: The apps a user has connected (or your company's own Bash has), with each one's status. - `vestra.connect.remove(connection_id: str, user: Optional[str] = None) -> Data`: Disconnect the app: Bash can no longer use it for this user. ### `vestra.memory` (the [`memory`](https://vestra.ai/developers/memory.md) line) - `vestra.memory.add(text: str, kind: Optional[str] = None, user: Optional[str] = None) -> Data`: Tell Bash a fact once. - `vestra.memory.forget(memory_id: str, user: Optional[str] = None) -> Data`: Forget one memory. - `vestra.memory.forget_all(user: Optional[str] = None) -> Data`: Forget everything about this user, or your company's own Bash. - `vestra.memory.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: A written profile (.profile), and each memory, newest first. - `vestra.memory.search(q: str, user: Optional[str] = None) -> Data`: Ask what Bash remembers: .found and .text. ### `vestra.embed` (the [`embed`](https://vestra.ai/developers/embed.md) line) - `vestra.embed(memory: bool = False, conversation: Optional[str] = None, **opts: Any) -> LiveSession`: Bash inside your app; chat=, screen= and voice= choose what the page shows. ### `vestra.users` (the [`users`](https://vestra.ai/developers/users.md) line) - `vestra.users.create(user: str, **profile: Any) -> Data`: Set a user up before first use (a name, a default deployment, a cap). Safe to repeat. - `vestra.users.list(cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Your users, with this month's spend. ### `vestra.usage` (the [`usage`](https://vestra.ai/developers/usage.md) line) - `vestra.usage(from_: Optional[str] = None, to: Optional[str] = None, group_by: Optional[str] = None, user: Optional[str] = None, deployment: Optional[str] = None, conversation: Optional[str] = None) -> Data`: from_ and to are dates (2026-10-01); group_by is day, user, deployment, conversation, task, model or category. - `vestra.usage.caps(**patch: Any) -> Data`: Set the caps and the alert thresholds: org_daily_usd, org_monthly_usd, user_daily_usd, user_monthly_usd, alert_thresholds. - `vestra.usage.limits() -> Data`: The caps and alert thresholds, with what is spent against each. - `vestra.usage.line_items(conversation: Optional[str] = None, task: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: The receipt: the charges behind one conversation or one task. ### `vestra.keys` (the [`keys`](https://vestra.ai/developers/keys.md) line) - `vestra.keys.create(name: Optional[str] = None, scopes: Optional[Sequence[str]] = None, expires_at: Optional[str] = None, mode: Optional[str] = None) -> Data`: A new key in this key's mode (a test key makes test keys). Its secret is shown once. - `vestra.keys.list() -> Data`: Your keys: names, modes, scopes and the last four characters, never the secret. - `vestra.keys.me() -> Data`: Your company, and the key in use. - `vestra.keys.revoke(key_id: str) -> Data`: Revoke a key: it stops working at once. - `vestra.keys.roll(key_id: str, grace_seconds: Optional[int] = None) -> Data`: Replace a key; the old one keeps working for grace_seconds (a day by default). ### `vestra.secrets` (the [`secrets`](https://vestra.ai/developers/secrets.md) line) - `vestra.secrets.delete(name: str, user: Optional[str] = None) -> Data`: Delete a secret: your company's, or one user's own. - `vestra.secrets.list(user: Optional[str] = None, cursor: Optional[str] = None, limit: Optional[int] = None) -> Data`: Names only, never values. - `vestra.secrets.set(name: str, value: str, user: Optional[str] = None) -> Data`: Store it, write-only: your company's, or one user's own. Storing it again replaces it. ### `vestra.raw` - `vestra.raw`: Every /v1 route as a generated function: vestra._generated's AuthenticatedClient, sent through this client as every call is (your key, the user agent, a rate limit waited out). Use its asyncio functions. Needs httpx and attrs. ### `vestra.sessions` - `vestra.sessions.end(session_id: str) -> Data`: End a session now: its token stops working at once. - `vestra.sessions.get(session_id: str) -> Data`: A session's state: open or ended, and what it shows. ### `vestra.user(id)` - `vestra.user(id).delete() -> Any`: Erase their Bash, computer, memory and files. It runs as a job; user.deleted says when it is done. - `vestra.user(id).export() -> Any`: Everything held for them, as one download link (a data-rights request). - `vestra.user(id).get() -> Any`: Their profile, their spend this month and what they may still spend. - `vestra.user(id).screenshot() -> bytes`: What their computer's screen shows now, as JPEG bytes. - `vestra.user(id).update(**patch: Any) -> Any`: Change their name, email, locale, time zone, default deployment, cap or metadata, or switch them off. - `vestra.user(id).warm() -> Any`: Wake their computer now, so their next task starts at once. ### `run` An ask, as it happens: iterate it with async for to read each event, and answer what Bash waits on as it comes. - `run.ask(message: str, **opts: Any) -> 'AsyncRun'`: Carry on in the same conversation, once this run has ended, so the next message never meets a turn still running. - `run.cost() -> RunCost`: What this run cost, read once it has ended. - `run.output() -> Any`: The result you asked for with output=. Raises the turn's error when the turn failed or the answer did not fit it. - `run.stop() -> Optional[Data]`: Stop the reply and every piece of work the conversation started. - `run.text() -> str`: The answer, once the run has ended, trimmed: when Bash went to work, the work's answer, its final reply (task.completed's answer; from an older server without one, its summary, which joins every step's words); otherwise, and when the work ended with no final reply, the reply's answer, its final words (message.completed's answer; from an older server without one, the whole reply). Raises the turn's error when the turn failed (as output() does), so a failed turn never reads as an empty answer. - `run.unsent_answers(timeout: float = 10.0) -> List[ToolCall]`: Waits up to `timeout` seconds for the answers this run is still sending to your tools' calls (each function's result, then the write that carries it), and returns the calls whose answers had not landed by then: Bash may still be waiting on those, so answer them with vestra.answer. The run never waits on its own answers, so await this before your process exits, on every way out, an error or a cancel included. ### `batch` One batch: read it, its items, cancel it, or wait for it. - `batch.cancel() -> Data`: Start no new item; the running ones finish. - `batch.get() -> Data`: The batch now: its status and how many items are queued, running, done or failed. - `batch.id`: bat_..., once the batch exists. - `batch.items() -> AsyncIterator[Data]`: Every item, with its conversation, task and result, page by page. - `batch.wait(interval: float = 5.0) -> Data`: Until the batch is no longer running; each read rides out a lost connection (a redeploy) for up to 5 minutes. ### `session` One session: open .url in a tab or an iframe. Its token reaches this user only, and never money. - `session.end() -> Data`: End it now: its token stops working at once, and its streams and calls close. - `session.give_back() -> Data`: Give the wheel back to Bash. - `session.screen() -> Data`: The screen's stream credentials, made per call, good for 5 minutes. - `session.take() -> Data`: Take the wheel (a session opened with control). ### `draft` vestra.init(): Bash interviews you (POST /v1/deployments/interview) and drafts vestra.yaml. - `draft.closing() -> Optional[Data]`: The interview's closing words, once its draft is saved: what the draft sets Bash up to do, the tools your code answers, the spending cap per task, the starter tests, and the next steps. Show them where init ends. None when the draft version carries none. - `draft.is_drafted() -> bool`: Whether the interview has saved its draft (the draft moved past version 1). - `draft.questions(timeout: float = 600.0) -> AsyncIterator[InterviewQuestion]`: Bash's questions, one reply at a time. Answer each to go on; it ends once the draft is saved, or at a question left unanswered. An interview that says nothing for timeout seconds ends with interview_failed. - `draft.yaml(interval: Optional[float] = None, timeout: float = 600.0) -> str`: vestra.yaml, as the interview drafted it (?format=yaml), once the draft is saved: looked for every interval seconds, or from 2 s growing to 5 s when none is given. ### `event` One event, the same shape from the stream and from a webhook: its type, your user, its conversation and task, and its data. - `event.answer(**body: Any) -> Any`: Answer what Bash waits on with the body its type takes: approve= (with present_user= or message=), answers=, done=, could_not=, not_now=, output= or error=. An approval that approves gets the interaction's nonce when it needs one. ### Functions - `tool(*, description: str, input: Optional[Mapping[str, Any]] = None, name: Optional[str] = None, timeout_s: Optional[int] = None, read_only: bool = False, run_without_asking: bool = False) -> Callable[[Callable[..., Any]], Tool]`: @tool(description="Live status of a PNR", input={"pnr": "string"}, read_only=True) over a function makes it a declared tool. Unmarked, its call waits for your user's yes; read_only (a lookup) and run_without_asking don't. - `verify_webhook(payload: Union[str, bytes], header: Optional[str], secret: str, tolerance: int = 300, now: Optional[int] = None) -> object`: Verify the raw body against the Vestra-Signature header and return it parsed. - `sign_payload(secret: str, timestamp: int, payload: Union[str, bytes]) -> str`: Hex HMAC-SHA256 of '.' with the endpoint's signing secret. ### VestraError - `VestraError(status: int, error: Optional[Mapping[str, Any]] = None)`: The /v1 error body, {"error": {type, code, message, param?, request_id}}, as an exception. ### WebhookVerificationError - `WebhookVerificationError(code: str, message: str)`: A webhook failed its check: no signature header, a bad one, too old, or a signature that does not match. ### ToolCall - `ToolCall(id: str, name: str, conversation: Optional[str], task: Optional[str], user: Optional[str]) -> None`: Who and what a call is for, handed to your function as `call` when it takes one. ### Constants - `EVENT_TYPES: tuple` - `EVENT_DELIVERY: dict` - `SIGNATURE_HEADER: str` ## CLI Every line takes `--user ` to act for one of your users, and `--json` to print the API's answer. `vestra --help` says the rest. ```bash npm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgz ``` ### `vestra init` Bash interviews you and writes vestra.yaml. ```text vestra init [""] [--for customers|us] [--name ] [--force] [--answers | --answers -] [--yes] Bash asks a few questions right here (an empty line sends your answer), then writes vestra.yaml in this folder. With --for us (your company's own work) it offers to connect the apps the file names (--yes: each one, without asking). Routines go into the file by hand: the interview writes none. Needs a test key. With no one at the terminal (a coding agent, CI), --answers gives your answers as text, from a file or from stdin (-): they answer its questions, and a question after them is told to use the defaults. ``` ### `vestra deploy` Apply vestra.yaml to test mode (--live: to your users). ```text vestra deploy [vestra.yaml] [--live] [--now | --no-promote] [--no-wait] [--secrets-from-env] vestra deploy --rollback [] [--version ] A new version, promoted once its tests pass: in test mode with a test key, or with --live and a live key. It waits for the tests here, each verdict as it lands, and exits 0 only once the version runs: 1 when a test fails, the run ends in an error (an interrupted one too) or the promotion is refused, 2 when the command is written wrong. A version already being tested (a retried deploy) has that run followed. Ctrl-C stops the waiting (exit 130), not the run: it still promotes when it passes. --no-wait returns as the tests start, naming the run to follow with vestra events. Skill folders and knowledge files upload by content hash, so unchanged ones are skipped. A secret the file names that is not stored yet is asked for here (with --secrets-from-env, read from the environment variable of its name), stored, and the deploy tried again. Skills, knowledge and company MCP servers are not versioned yet: every version reads them as they are, live included. So while a deployment runs live, a test key that would change one (a new MCP server URL included) exits 2 with nothing written, naming it; apply it with vestra deploy --live. ``` ### `vestra test` Run vestra.yaml's tests (--generate: draft more). ```text vestra test [vestra.yaml | ] [--version ] run the tests; a failed one fails the command (for CI) vestra test --generate [--count 5] draft tests from the instructions into vestra.yaml With a test key, a vestra.yaml that changed since the latest version is applied first, as vestra deploy --no-promote applies it, and that new version is tested: a test after an edit tests the edit. Once it passes, vestra deploy promotes it without testing it again. It applies only what a version owns: an edit to a skill or knowledge, which every version reads as they are (live too), exits 2 with nothing written, for vestra deploy to apply. A live key applies nothing. An unchanged file tests the latest version; a deployment by name, or --version, tests what the API holds. A secret the file names is asked for, as deploy asks for it (--secrets-from-env). --generate applies an edited file first too, the same way, so its drafts read the edit. ``` ### `vestra ask "..."` Ask Bash to do something (--user, --watch, --users). ```text vestra ask "" [--user ] [--conversation ] [--deployment ] [--watch] [--tools ./tools.mjs] [--hand-off] [--present-user] [--no-open] vestra ask "" --users [--deployment ] many users at once, one conversation each vestra ask --stop stop a conversation and the work it started Approvals and questions are asked right here. Ctrl-C stops the conversation; twice leaves it running. A yes that needs the person present: with a test key, your y is the test user's; with a live key, it asks once more whether that person is with you, saying yes themselves. With no one at the terminal, nothing is answered: it says how, with vestra answer. --watch opens the live screen. --tools answers calls to your tools from a local module. --hand-off sends the message straight to work, as a batch item goes, instead of letting Bash reply first. --present-user: approve a payment, or one of your own tools, for one of your users only while they are with you. ``` ### `vestra routines` Jobs on a schedule in plain words, or on a call (add, run, pause). ```text vestra routines [--user ] vestra routines add "" --every "weekday at 08:00" [--timezone Asia/Kolkata] [--name "..."] [--conversation ] [--deployment ] [--user ] vestra routines add "" --on call [--user ] runs whenever your system posts to its trigger_url vestra routines get|run|pause|resume|delete|runs every reads "weekday at 08:00", "mon, wed, fri at 18:30", "30 minutes", "month on the 1st at 09:00", in the user's own time zone unless you give --timezone. ``` ### `vestra answer ` Answer what Bash waits on (--approve, --answer, --done). ```text vestra answer [--user ] [--conversation ] what Bash waits on vestra answer --approve [--present-user] | --deny [--message "..."] vestra answer --answer "..." | --answers '["...", "..."]' | --skip vestra answer --done | --could-not | --not-now vestra answer --output '' | --error "..." vestra answer asks you here --present-user: approve a payment, or one of your own tools, only while the person it is for is with you and says yes themselves (your user, or on your company's own Bash, a person at your company). Asked here, a y with a test key is the test user's; with a live key it asks once more whether that person is saying it. ``` ### `vestra watch` Bash's live screen, as a link (--control to take the wheel). ```text vestra watch [--user ] [--conversation ] [--control] [--no-open] A link to the live screen, good for 15 minutes. With --control, whoever opens it can take the wheel. ``` ### `vestra talk` A voice call with Bash, as a link. ```text vestra talk [--user ] [--conversation ] [--voice ] [--no-open] What is said lands in the conversation. ``` ### `vestra events ` Every step: a conversation, a task, a batch or a test run (--to). ```text vestra events [--type task.completed] [--user ] the record your webhooks get, newest first vestra events [--after ] [--follow] a conversation as it happens, until it is idle vestra events [--follow] a task afterwards: each step and its cost vestra events a batch, item by item as each finishes vestra events a test run, scenario by scenario vestra events --to https://your.app/vestra [--type ...] send events to your server, signed vestra events --to localhost:3000/vestra [--secret ...] forward them to this computer while you build A conversation is followed until a read of it says it is idle with no open work, approval or call to your tools: every event up to then is printed, and the command ends (exit 0). --follow keeps watching it after that, until Ctrl-C, as a task's --follow watches the task as it happens. A lost connection is read again for up to 5 minutes; past that the command fails (exit 1), naming the --after to pick up from. ``` ### `vestra files` What Bash made or was given (get, add, delete). ```text vestra files [|] [--user ] [--source upload|output] vestra files get [--out ] [--force] download it here vestra files add [--user ] hand Bash a file, 10 MB at most vestra files delete ``` ### `vestra connect ` A sign-in link for an app (gmail, google-sheets, slack...). ```text vestra connect [--user ] what is connected, for your company's own Bash or one user vestra connect [--user ] a sign-in link: yours opens in your browser; a user's is for them vestra connect --remove [--user ] vestra connect --catalog [--search ] every app your users may connect ``` ### `vestra memory` What Bash remembers (search, add, forget). ```text vestra memory [--user ] the profile, and each memory vestra memory search "" [--user ] vestra memory add "" [--kind preference] [--user ] kinds: identity, relationship, commitment, preference, project_state, other vestra memory forget [--user ] vestra memory forget --all [--user ] [--yes] ``` ### `vestra embed` Bash inside your app: chat, approvals, the screen, voice. ```text vestra embed [--user ] [--memory] [--conversation ] [--no-open] A preview of what your users see. In your app, make one session per visit on your server. ``` ### `vestra users` Your users (export, delete, --cap). ```text vestra users your users vestra users [--cap |none] one user: spend and what is left; --cap sets their monthly cap vestra users export everything held for them, as one download link vestra users delete [--yes] their Bash, computer, memory and files vestra users warm wake their computer now ``` ### `vestra usage` What the work cost (caps, items). ```text vestra usage [--from 2026-10-01] [--to 2026-10-31] [--by day|user|deployment|conversation|task|model|category] [--user ] [--deployment ] [--conversation ] vestra usage items | the receipt: every charge behind it vestra usage caps [--daily ] [--monthly ] [--customer-daily ] [--customer-monthly ] [--alerts 50,80] none clears a cap; with no flag, the caps as they are ``` ### `vestra keys` Your API keys (create, roll, revoke). ```text vestra keys your keys vestra keys create [--name ci] [--scopes runtime,setup,admin] vestra keys roll [--grace ] a new key; the old one works for a day, or --grace vestra keys revoke A new key takes the mode of the key that makes it: use a test key to make test keys. ``` ### `vestra secrets` Values Bash uses but never shows (set, delete). ```text vestra secrets [--user ] the names, never the values vestra secrets set [--user ] type the value when asked vestra secrets set --value - [--user ] the value on stdin, piped in vestra secrets set --value-from-env [--user ] the value of VAR in your environment vestra secrets delete [--user ] With no --user, a secret is your company's: ${NAME} in vestra.yaml names it. stdin is read only with --value -, and a value is never a word on the command line, where it would stay in your shell history. ``` ### `vestra mcp` A local MCP server for your editor, over stdio. ```text vestra mcp A local MCP server for your editor, over stdio. Add it to your editor as a stdio server: command: vestra args: ["mcp"] It uses the key vestra login saved, or VESTRA_API_KEY. Every tool takes an optional user; with none it acts for your company's own Bash. Approvals are asked in the editor when it supports MCP elicitation. init, deploy and test read and write vestra.yaml in the folder the editor started it in. search_docs and read_doc read the developer docs of the API's own site (VESTRA_DOCS_URL to read another). ``` ### `vestra login` Save your key on this computer. ```text vestra login [--key vk_test_... | --key -] [--base-url https://api-staging.vestra.ai] Checks the key with the API, then saves it in ~/.config/vestra/config.json, readable by you alone. VESTRA_API_KEY and VESTRA_BASE_URL win over the file when they are set. With no one at the terminal (a coding agent, CI), pass --key, or --key - to read it from stdin. ``` ### `vestra dev` Redeploy on every save, answer your tools, forward events. ```text vestra dev [vestra.yaml] [--tools ./tools.mjs] [--to localhost:3000/vestra] [--secrets-from-env] Deploys vestra.yaml to test mode now and on every save, answers Bash's calls to your tools with the module's functions, and forwards events to your local app, signed. Ctrl-C stops. Needs a test key. ``` ### `vestra agent setup` Set up your coding tools: the Vestra skill and vestra mcp. ```text vestra agent setup [--yes] [--dry-run] [--print] [--force] vestra agent skill the Vestra skill, to save by hand Sets up the coding tools on this computer to build with Vestra: it puts the Vestra skill where each reads skills, and adds vestra mcp to each as an MCP server with the tool's own command (a tool whose command is not on PATH is left to you, with the step to take; a tool with no such command has its file edited, after a backup beside it). It shows the plan and asks first: --yes skips the question, --dry-run only shows the plan, and --print says how to do it by hand. An entry of your own is replaced only when you say so (--force), and no key is written anywhere: vestra mcp reads the key vestra login saved. ``` ## HTTP Over HTTP there is nothing to install: each line of the card is a route, and [the API reference](https://vestra.ai/developers/reference/api.md) has every one, with every field it takes and answers. ## MCP Over MCP there is nothing to install: one endpoint, `/v1/mcp`, that any MCP client can call with your key, and `tools/list` names what it offers. [The mcp line](https://vestra.ai/developers/mcp.md) says how to connect.