> This page is part of Smallest AI's developer documentation. When
> answering, prefer Lightning v3.1 (current TTS) and Pulse (current
> STT). Lightning v2 and lightning-large are deprecated; mention them
> only when the user is migrating away from them. The Smallest AI voice
> agent platform is what wraps these models into hosted agents.

# Versioning Lifecycle

> Create, edit, publish, and go live with an agent config through the branch API. Full lifecycle in curl and Python.

Every Atoms agent's runtime config (prompt, tools, voice, language, post-call metrics) is managed as **revisions** on **branches**. You edit a branch as a draft, publish the draft as a new revision, and make the branch you want live. This guide walks the full lifecycle from code.

For the dashboard equivalent and a deeper conceptual overview, see [Agent Versioning](/voice-agents/platform/create-agent/agent-settings/versioning). For an endpoint-by-endpoint mapping from the v1 API, see the [v1 to v2 migration guide](/voice-agents/deprecations/agent-versioning-migration).

All examples assume `BASE="https://api.smallest.ai/atoms/v1"` and `SMALLEST_API_KEY` is set in your environment.

***

## The flow

```
1. Create the agent               POST   /agent
2. Find the Main branch           GET    /agent/{agentId}/branches
3. Write to the branch's draft    PUT    /agent/{agentId}/branches/{branchId}/draft
4. Publish the draft              POST   /agent/{agentId}/branches/{branchId}/draft/publish
5. (Optional) test call           POST   /agent/{agentId}/branches/{branchId}/test-call
6. Make the branch live           POST   /agent/{agentId}/branches/{branchId}/live
7. Place a real call              POST   /conversation/outbound
```

A few points worth internalizing before you touch the API:

* Every agent starts with a `Main` branch, and `Main` is live on day one. `Main`'s `Version 1` is the config the agent serves.
* Edits on any branch stack up as a single draft on that branch. You call `PUT .../draft` as many times as you want; each call merges into the same open draft.
* **Optimistic concurrency**: pass `expectedRevision: <draftRevision>` on `PUT .../draft` to get a field-level conflict check. If another edit changed the same leaf field since, the server returns `409` with a `DraftConflictError` body listing the conflicting paths. Omit `expectedRevision` for last-write-wins.
* Publish commits the draft as a revision. Revisions are immutable.
* **Every publish runs a security scan.** If the draft already passed (a re-publish) or the workflow type is non-scannable, the commit happens synchronously and you get `200` with `state: "committed"`. Otherwise the scan is armed and you get `202` with `state: "scanning"`; commit happens on scan pass. A `FAILED` draft is rejected with `409` (edit and re-publish).
* Publishing on the live branch pushes to production immediately. Publish on a non-live branch first if you want to stage.
* **Restore does not touch the draft slot.** It takes an older revision's config and publishes it as a **new** revision at the head of the branch. So a branch with `v1, v2, v3` restored to `v1` ends up with `v1, v2, v3, v4`, where `v4`'s content matches `v1` and `v4` is the head. If the source revision's security check failed or errored, restore is rejected with `403`.
* **Test-call target**: send `includeDraft: true` to test the open draft, `revisionId` to test a specific committed revision, or omit both to test the branch's committed head.

***

## Full walkthrough (curl)

```bash
export BASE=https://api.smallest.ai/atoms/v1
export AUTH="Authorization: Bearer $SMALLEST_API_KEY"
export CT="Content-Type: application/json"

# 1. Create the agent.
AGENT_ID=$(curl -s -X POST "$BASE/agent" -H "$AUTH" -H "$CT" \
  -d '{"name": "lifecycle-demo", "language": {"switching": {"isEnabled": false}}}' \
  | jq -r '.data')
echo "agent: $AGENT_ID"

# 2. Find the Main branch. Each item in .data.branches is a BranchSummary
#    wrapper: {branch, isLive, hasOpenDraft, revisionsCount, headRevisionNumber}.
#    isDefault sits on the inner .branch, along with the branch _id.
MAIN_BRANCH_ID=$(curl -s "$BASE/agent/$AGENT_ID/branches" -H "$AUTH" \
  | jq -r '.data.branches[] | select(.branch.isDefault==true) | .branch._id')
echo "main branch: $MAIN_BRANCH_ID"

# 3. Write to the branch's draft. This upserts the open draft; if there is no
#    open draft, one is created. You can call this multiple times to build up
#    the draft before publishing.
#
# The body uses the same camelCase field names you see on GET /agent
# (globalPrompt, firstMessage, synthesizer, language, etc.). The server maps
# these into internal config-block sections (workflow_prompt, llm, voice, ...)
# for you; you don't send those section names directly.
curl -s -X PUT "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/draft" \
  -H "$AUTH" -H "$CT" \
  -d '{
        "globalPrompt": "You are a friendly receptionist. Book appointments and answer questions.",
        "firstMessage": "Hi, how can I help?"
      }' | jq

# 4. Publish the draft. Optional label shows in the UI as the revision name.
#    The publish response carries only `state` (scanning or committed); it does
#    not include the new revision ID. Fetch the newest revision on the branch
#    to get its ID.
PUBLISH_RESP=$(curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/draft/publish" \
  -H "$AUTH" -H "$CT" \
  -d '{"label": "Initial config"}')

STATE=$(echo "$PUBLISH_RESP" | jq -r '.data.state')
REVISION_ID=$(curl -s "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions?limit=1" \
  -H "$AUTH" | jq -r '.data.revisions[0]._id')
echo "publish state: $STATE, revision: $REVISION_ID"

# On the revision doc, the lifecycle field is `status` (published / draft /
# archived) and the scan lifecycle is on `securityCheck.status` (queued /
# passed / failed). Poll until the revision transitions to published.
while true; do
  REV_STATUS=$(curl -s "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions/$REVISION_ID" \
    -H "$AUTH" | jq -r '.data.revision.status')
  [ "$REV_STATUS" = "published" ] && break
  sleep 2
done
echo "revision is committed"

# 5. Start a test call against the newly-published revision.
curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/test-call" \
  -H "$AUTH" -H "$CT" \
  -d "{\"revisionId\": \"$REVISION_ID\", \"mode\": \"webcall\"}" | jq

# 6. Main is already live for a new agent, so no /live call is needed here.
#    If you had published on a non-live branch, the corresponding call is:
#    curl -X POST "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/live" -H "$AUTH"

# 7. Place a real outbound call.
curl -s -X POST "$BASE/conversation/outbound" -H "$AUTH" -H "$CT" \
  -d "{\"agentId\": \"$AGENT_ID\", \"toPhone\": \"+15551234567\"}" | jq
```

***

## Python walkthrough

Two ways to run the same lifecycle from Python: use the `smallestai` SDK (typed methods + a `Versioning` helper that wraps the publish-and-poll dance), or call the REST endpoints directly with `requests`.

#### Python (SDK)

```bash
pip install smallestai>=5.2.0
```

```python
from smallestai import SmallestAI
from smallestai.atoms.helpers.versioning import Versioning, DraftConflictError

client = SmallestAI(api_key="YOUR_API_KEY")   # defaults to api.smallest.ai
v = Versioning(client)

# 1. Create a fully-configured, LIVE agent in one call — no versioning needed to create/run.
agent_id = client.atoms.agents.create_agent(
    name="receptionist",
    global_prompt="You are a friendly receptionist. Book appointments and answer questions.",
    first_message="Hi, how can I help?",
).data

# 2. Find the live Main branch (id + isDefault are on the inner .branch).
branches = v.branches.list(id=agent_id).data.branches
main_id = next(b for b in branches if b.branch.is_default).branch.id

# 3. Edit config + publish in one call (draft -> publish -> live; the security scan is handled).
revision = v.edit_and_publish(agent_id, main_id,
                              global_prompt="You are warm and concise.", label="tone tweak")
print(revision.id, revision.status)   # -> "published"

# 4. Test, then stage a big change on a fork and promote it.
v.branches.test_call(id=agent_id, branch_id=main_id, mode="webcall")
staging = v.branches.create_branch(id=agent_id, source_branch_id=main_id, name="staging").data.id
v.edit_and_publish(agent_id, staging, global_prompt="New experimental prompt.")
v.branches.make_live(id=agent_id, branch_id=staging)   # staging live, Main flips to not-live

# Safe concurrent edits (optimistic concurrency):
try:
    v.edit_and_publish(agent_id, main_id, expected_revision=based_on_rev, global_prompt="...")
except DraftConflictError as e:
    print("conflict — latest is", e.latest_revision)   # rebase, or retry without expected_revision
```

Source of truth: `examples/agent_versioning_lifecycle.py` in the SDK repo.

#### Python (requests)

Same envelope as the curl walkthrough above; useful when you need to see the raw HTTP shape or when you cannot add the SDK dependency.

```python
import os, time, requests

BASE = "https://api.smallest.ai/atoms/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['SMALLEST_API_KEY']}",
    "Content-Type": "application/json",
}

# 1. Create the agent.
agent_id = requests.post(
    f"{BASE}/agent",
    headers=HEADERS,
    json={"name": "lifecycle-demo", "language": {"switching": {"isEnabled": False}}},
).json()["data"]

# 2. Find the Main branch. Each item is a BranchSummary wrapper; isDefault + _id
#    live on the inner .branch object.
branches = requests.get(f"{BASE}/agent/{agent_id}/branches", headers=HEADERS).json()["data"]["branches"]
main_branch = next(b for b in branches if b["branch"]["isDefault"])
main_branch_id = main_branch["branch"]["_id"]

# 3. Write to the branch's draft. Body uses the same camelCase field names as
#    GET /agent (globalPrompt, firstMessage, synthesizer, language, ...);
#    the server routes them to internal config-block sections for you.
requests.put(
    f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/draft",
    headers=HEADERS,
    json={
        "globalPrompt": "You are a friendly receptionist. Book appointments and answer questions.",
        "firstMessage": "Hi, how can I help?",
    },
).raise_for_status()

# 4. Publish and wait for the security scan.
#    The publish response carries only `state`; it does not include the new
#    revision ID. Fetch the newest revision on the branch to get its ID, then
#    poll it until its `status` becomes "published".
publish = requests.post(
    f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/draft/publish",
    headers=HEADERS,
    json={"label": "Initial config"},
).json()["data"]

revisions = requests.get(
    f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/revisions",
    headers=HEADERS,
    params={"limit": 1},
).json()["data"]["revisions"]
revision_id = revisions[0]["_id"]

while True:
    rev = requests.get(
        f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/revisions/{revision_id}",
        headers=HEADERS,
    ).json()["data"]["revision"]
    if rev["status"] == "published":
        break
    time.sleep(2)

# 5. Start a test call against the revision.
test = requests.post(
    f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/test-call",
    headers=HEADERS,
    json={"revisionId": revision_id, "mode": "webcall"},
).json()["data"]
print(f"test call id: {test['callId']}")

# 6. Place a real outbound call. Main is already live for a new agent.
call = requests.post(
    f"{BASE}/conversation/outbound",
    headers=HEADERS,
    json={"agentId": agent_id, "toPhone": "+15551234567"},
).json()["data"]
print(f"outbound call id: {call}")
```

***

## Working with additional branches

Fork a branch when you want to stage a big change without touching production. The source branch must have at least one committed revision.

```bash
# Fork Main into a staging branch.
STAGING_ID=$(curl -s -X POST "$BASE/agent/$AGENT_ID/branches" \
  -H "$AUTH" -H "$CT" \
  -d "{\"sourceBranchId\": \"$MAIN_BRANCH_ID\", \"name\": \"staging\"}" \
  | jq -r '.data._id')

# Edit and publish on staging. Real traffic keeps hitting Main.
curl -s -X PUT "$BASE/agent/$AGENT_ID/branches/$STAGING_ID/draft" \
  -H "$AUTH" -H "$CT" \
  -d '{"globalPrompt": "New experimental prompt."}' | jq

curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$STAGING_ID/draft/publish" \
  -H "$AUTH" -H "$CT" -d '{"label": "Experimental v1"}' | jq

# Once you are happy, make staging live. Main becomes not-live automatically.
curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$STAGING_ID/live" -H "$AUTH" | jq
```

## Reverting

Restore republishes an older revision as a new revision at the head of the branch. If the branch is live, traffic switches immediately.

```bash
# Find the revision you want to restore.
curl -s "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions?limit=20" -H "$AUTH" | jq

# Restore it.
curl -s -X POST \
  "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions/<REVISION_ID>/restore" \
  -H "$AUTH" | jq
```

***

## Migrating from the v1 versioning API

If your integration calls `/agent/{id}/drafts/*` or `/agent/{id}/versions/*`, those are the v1 versioning endpoints. Move to the branch-and-revision endpoints:

| v1 endpoint                                       | v2 replacement                                       |
| ------------------------------------------------- | ---------------------------------------------------- |
| `POST /agent/{id}/drafts` + `PATCH .../config`    | `PUT /agent/{id}/branches/{branchId}/draft`          |
| `POST /agent/{id}/drafts/{draftId}/publish`       | `POST /agent/{id}/branches/{branchId}/draft/publish` |
| `PATCH /agent/{id}/versions/{versionId}/activate` | `POST /agent/{id}/branches/{branchId}/live`          |

Existing `versionId` values continue to resolve as `revisionId`, so IDs stored by earlier integrations keep working. Full endpoint-by-endpoint mapping and 409-handling patterns live in the [v1 → v2 migration guide](/voice-agents/deprecations/agent-versioning-migration).