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