Going live
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.
Get a live key
Give CI its own key
A setup key deploys and tests, and can never act for a user.
What happens Its secret is shown this once: keep it in your CI's secrets.
Show outputHide output
The CLI prints
Made key_3vd (ci), live mode. Its secret, shown this once: vk_live_…
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 oncelive = 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# 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# 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"] }'Keys stay with people: no MCP tool makes one.
# 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"] }'Cap the spend
Caps are kept per mode, so set them again for live (usage).
What happens Work past a cap is refused with 402, and usage.cap_reached reaches your webhooks.
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 });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)# 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# 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 }'Caps stay with people: set them from an SDK, the CLI or HTTP.
# 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 }'Point your webhooks at production
Webhooks are kept per mode too: your test endpoint never hears a live user.
What happens Live events reach your server, signed with this endpoint's own secret.
Show outputHide output
The CLI prints
Sending task.completed, usage.cap_reached to https://api.tripnest.example/vestra as whk_8nd. Its signing secret, shown this once: whsec_…
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);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)# 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# 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"] }'Webhooks are set up from an SDK, the CLI or HTTP.
# 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"] }'Ship it live
A live deploy plays its tests first (deploy).
What happens Only a pass puts the new version in front of your users. A failure leaves the version before running.
Show outputHide output
The CLI prints
travel-help version 5: changed instructions. promotes in live mode once its tests pass: vestra events trun_6tw.
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);}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)# Live once its tests pass.VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live# 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" }] } } }] }'Going live is a person's to type: /v1/mcp and vestra mcp deploy to test mode only.
# 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" }] } } }] }'Know the way back
A rollback puts the version before back at once.
What happens The version before is live again, with no tests to wait for.
Show outputHide output
The CLI prints
travel-help: version 4 is live again.
// 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');# 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")# Back to the version before, at once: it needs a live key.VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --rollback travel-help# 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"/v1/mcp never deploys or rolls back: that stays with you, with a live key.
# 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"The whole thing
The checklist as one program, run once before your first live user.
What happens Your users meet a tested version, inside your caps, with your server hearing every step.
import { Vestra } from '@vestra/sdk'; const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY }); // Give CI its own keyconst ci = await live.keys.create({ name: 'ci', scopes: ['setup'] });console.log(ci.secret); // Cap the spendawait live.usage.caps({ org_monthly_usd: 500, user_monthly_usd: 20 }); // Point your webhooks at productionawait live.events.to('https://api.tripnest.example/vestra', { events: ['task.completed', 'usage.cap_reached'],}); // Ship it liveconst applied = await live.deploy('vestra.yaml', { live: true });if (applied.tests) { const run = await live.test.wait(applied.tests.id); console.log(run.outcome);}import os from vestra import Vestra live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"]) # Give CI its own keyprint(live.keys.create(name="ci", scopes=["setup"]).secret) # Cap the spendlive.usage.caps(org_monthly_usd=500, user_monthly_usd=20) # Point your webhooks at productionlive.events.to( "https://api.tripnest.example/vestra", events=["task.completed", "usage.cap_reached"],) # Ship it liveapplied = live.deploy("vestra.yaml", live=True)if applied.tests: print(live.test.wait(applied.tests.id).outcome)# Give CI its own keyVESTRA_API_KEY=$VESTRA_LIVE_KEY vestra keys create --name ci --scopes setup # Cap the spendVESTRA_API_KEY=$VESTRA_LIVE_KEY vestra usage caps --monthly 500 --customer-monthly 20 # Point your webhooks at productionVESTRA_API_KEY=$VESTRA_LIVE_KEY vestra events --to https://api.tripnest.example/vestra --type task.completed,usage.cap_reached # Ship it liveVESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live# Give CI its own keycurl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Cap the spendcurl -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 productioncurl "$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 livecurl -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" }] } } }] }'Going live stays with people: keys, caps and live deploys are never an agent's over MCP.
# Give CI its own keycurl "$VESTRA_BASE_URL/v1/keys" \ -H "Authorization: Bearer $VESTRA_LIVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "scopes": ["setup"] }' # Cap the spendcurl -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 productioncurl "$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 livecurl -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" }] } } }] }'