BashBrainTeammatesPricingDevelopersGet a test key

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.

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 <line> --help says the rest.

Over plain HTTP, curl is all you need.

npm install https://vestra.ai/developers/kit/vestra-sdk-0.1.8.tgz && npm pkg set type=module # TypeScriptpython3 -m venv .venv && source .venv/bin/activate                                           # Python: a virtual environment firstpip install https://vestra.ai/developers/kit/vestra-0.1.8-py3-none-any.whl                   # Pythonnpm install -g https://vestra.ai/developers/kit/vestra-cli-0.1.8.tgz                         # CLI
TypeScript
// 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 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
# Once the install line has run (it needs Node 20 or later), vestra is on your PATH: help lists every line.vestra help
HTTP
# Nothing to install. Is the API up? No key needed.curl "$VESTRA_BASE_URL/v1/health"
MCP
{  "mcpServers": {    "vestra": {      "type": "http",      "url": "https://api-staging.vestra.ai/v1/mcp",      "headers": { "Authorization": "Bearer vk_test_..." }    }  }}

Get a test key

Make a test key in the console, 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.

export VESTRA_API_KEY=vk_test_...export VESTRA_BASE_URL=https://api-staging.vestra.ai
TypeScript
import { Vestra } from '@vestra/sdk'; const vestra = new Vestra(); // reads VESTRA_API_KEY and VESTRA_BASE_URLconst me = await vestra.keys.me();console.log(me.org.name, me.mode); // your company, and test
Python
from vestra import Vestra vestra = Vestra()  # reads VESTRA_API_KEY and VESTRA_BASE_URLme = vestra.keys.me()print(me.org.name, me.mode)  # your company, and test
CLI
# Paste your test key when asked: vestra checks it with the API, then keeps it on this computer.vestra login
HTTP
curl "$VESTRA_BASE_URL/v1/me" \  -H "Authorization: Bearer $VESTRA_API_KEY"
HTTP

An MCP client sends your key itself, as a Bearer token, from its settings: the next step shows where it goes.

curl "$VESTRA_BASE_URL/v1/me" \  -H "Authorization: Bearer $VESTRA_API_KEY"

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 walks this path step by step.

TypeScript
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
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
# 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
# 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" }'
HTTP

An agent sets Bash up with the init and test tools. Shipping a version stays with you: vestra deploy, or deploy in the SDKs.

# 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" }'

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.

TypeScript
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
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
# 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
# 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
{  "method": "tools/call",  "params": {    "name": "ask",    "arguments": {      "user": "traveller-42",      "message": "My 6am flight got cancelled. Get me on the next one."    }  }}

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.

TypeScript
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
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
# vestra ask asks you right here. From a script or another terminal, answer by the id it names.vestra answer --user traveller-42vestra answer int_3j9 --approve --present-user
HTTP
# 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
{  "method": "tools/call",  "params": {    "name": "answer",    "arguments": { "interaction": "int_3j9", "approve": true, "present_user": true }  }}

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
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
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
# 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
# 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  }'
HTTP

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.

# 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  }'

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.

TypeScript
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
from flask import Flask, requestfrom 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
# 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
# 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"
HTTP

Webhooks are set up once, from an SDK, the CLI or over HTTP. An agent reads what happened with the events tool.

# 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"

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.

TypeScript
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
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
# The receipt behind one conversation, and one user's spend this month.vestra usage items conv_8f2kvestra users traveller-42
HTTP
# 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
{  "method": "tools/call",  "params": {    "name": "usage",    "arguments": { "from": "2026-10-01", "group_by": "user" }  }}