---
title: "Proxy API"
description: "The local agent API that viper proxy serves, for agents in other languages: capabilities, agents, registering definitions, storing files, the model-request headers, retries, and errors."
source: "Guardian Agent SDK"
---
`viper proxy` serves a small HTTP API to processes on its own machine. The Python SDK is built on it, and an agent in another language can use it directly: store any skill files, register the agent's definition, then name the agent and definition on each model request. This page describes version `v1`.

## Where it is

The API is at the proxy's own address, the base URL `viper proxy` prints when it starts, under `/__viper__/v1/`. Paths under `/__viper__/` are the proxy's own and are never forwarded to the gateway.

- **Local only:** the proxy listens on `127.0.0.1`. The API also refuses a call that addresses it by anything but `localhost` or a loopback IP, or that carries an `Origin` header, as a web page's requests do. Both are refused with `not_local`.
- **JSON in, JSON out:** request and response bodies are `application/json`.
- **Any endpoint** can also answer `405` `method_not_allowed` for the wrong method, with an `Allow` header, and an unknown path under `/__viper__/v1/` is `404` `not_found`.

## Errors

Every error from this API, and from a model request that names an agent, has this body and the `X-Viper-Error: 1` header, which marks errors the proxy made itself rather than ones it passed on:

```json
{
  "error": {
    "code": "agent_not_enrolled",
    "message": "agent `researcher` is not enrolled on this proxy",
    "hint": "enroll it with `viper agent add researcher`, then restart the proxy"
  }
}
```

`code` is what a client branches on, `message` is for people, and `hint`, when present, says what to do. A message may name the agent, definition or path the request named, but never repeats a value from a definition, prompt or file. An error for a model request that names no agent keeps the older `{"error": "<message>"}` shape, still marked. [Errors](./errors.mdx) lists every code, with its status.

## `GET /__viper__/v1/capabilities`

What this proxy supports. Ask once, at start-up, and check it before relying on the proxy.

- **`api`:** the API version, `"v1"`.
- **`viper`:** the proxy's Viper version, for messages only. Never compare it: check `features`.
- **`features`:** what the proxy does beyond the API version, by name, such as `"agent-headers"`. A name is added when its feature ships, and never removed ([Compatibility](./compatibility.mdx#the-proxy)).
- **`schemas`:** the definition schemas the proxy accepts, such as `"viper/agent-definition/v1"`.
- **`strict`:** whether model requests must name an agent (`viper proxy --require-agent-header`).
- **`agents`:** the agents enrolled on this proxy, sorted. The proxy reads them when it starts.
- **`limits`:** the bounds a definition must stay within, in camelCase, such as `definitionBytes` and `subAgentDepth` ([Limits](./limits.mdx)).

Responses: `200`, or `403` `not_local`.

## `GET /__viper__/v1/agents/{agent}`

One enrolled agent, as this proxy knows it.

- **`name`:** the agent's name.
- **`did`:** the DID its requests are signed as.
- **`definitions`:** the content IDs of the definitions registered for it since the proxy started, least recently registered first.
- **`latestDefinitionCid`:** the most recently registered definition, or `null`.

Responses: `200`, `403` `not_local`, or `404` `agent_not_enrolled`.

## `POST /__viper__/v1/collections`

Stores the files of one skill, or one source of memory the agent can't change, so a definition can name them by content ID. Store them before registering a definition that names them.

- **`files`:** each file's contents, base64-encoded (the standard alphabet, padded, with no line breaks), keyed by its path inside the skill or memory source, with `/` separators. Key a skill's files from its own directory, with its `SKILL.md` at the root, as a coding agent's skill is laid out, so the content ID matches Viper's for the same skill. A single memory file is keyed by the name the agent reads it by, such as `AGENTS.md`.

The response:

- **`cid`:** the collection's content ID, for a skill's or memory's `cid` in the definition.
- **`files`** and **`bytes`:** how many files it holds, and their size together.

The same files always give the same content ID. Responses: `201`, `403` `not_local`, `413` `collection_too_large`, `422` `collection_invalid` or `definition_contains_secret`, or `500` `internal_error` if the files couldn't be stored.

## `POST /__viper__/v1/agents/{agent}/definitions`

Registers a definition for an enrolled agent. The body is the definition, as JSON in the schema `viper/agent-definition/v1`: the schema is published at [`/schemas/agent-definition.v1.schema.json`](/schemas/agent-definition.v1.schema.json), and [Definitions](../concepts/definitions.mdx) and [Definition fields](../guides/definition-fields.mdx) describe each field. This one has a prompt, a tool, a skill and a sub-agent:

{/* generated file sdks/python/snippets/definition.json json */}

```json
{
  "schema": "viper/agent-definition/v1",
  "name": "report-writer",
  "description": "Writes reports from the internal docs.",
  "framework": {
    "name": "custom"
  },
  "models": [
    {
      "provider": "openai",
      "model": "gpt-4.1",
      "settings": {
        "temperature": 0
      }
    }
  ],
  "prompts": [
    {
      "role": "system",
      "content": "You write reports from the internal docs."
    }
  ],
  "tools": [
    {
      "name": "search_docs",
      "description": "Search the internal documentation.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string"
          }
        },
        "required": ["query"]
      }
    }
  ],
  "skills": [
    {
      "name": "triage",
      "cid": "<the cid POST /__viper__/v1/collections returned>"
    }
  ],
  "subAgents": [
    {
      "schema": "viper/agent-definition/v1",
      "name": "critic",
      "description": "Checks a draft's claims against the docs.",
      "framework": {
        "name": "custom"
      },
      "models": [
        {
          "provider": "openai",
          "model": "gpt-4.1",
          "settings": {
            "temperature": 0
          }
        }
      ],
      "prompts": [
        {
          "role": "system",
          "content": "You critique drafts and name each unsupported claim."
        }
      ]
    }
  ]
}
```

{/* end generated */}

- **Required:** `schema`, `name`, `framework` and at least the agent's model in `models`; `{"name": "custom"}` names no framework. The definition's `name` must be the agent's.
- **Names:** an agent's or sub-agent's name is 1 to 64 lowercase letters, digits, `.`, `_` and `-`, starting with a letter or digit.
- **Sub-agents** are whole definitions, with their own `schema`, `name`, `framework` and `models`, and a `description`, which a sub-agent must have.
- **Skills and memory** name their files by the `cid` the collections endpoint returned. A definition naming files the proxy doesn't hold is refused.
- **Prompts:** send every prompt's text inline. The proxy stores a prompt over the inline limit apart, and the definition it registers refers to it by `contentCid`. Prompts still count toward the prompt and registration limits ([Limits](./limits.mdx#prompts)).
- **Checked** against the schema, the proxy's rules, its limits and its credential scan, before anything is stored. A body that isn't valid JSON is `422` `definition_invalid` too.

The response:

- **`definitionCid`:** the definition's content ID as stored. Name it in `X-Viper-Definition` on model requests. Use this one rather than one you computed, since the proxy may have moved prompts out.
- **`created`:** whether this agent registered it for the first time since the proxy started.

The same definition always gives the same content ID, so registering it again is safe. Registrations last until the proxy restarts ([After a restart](#after-a-restart)).

Responses: `200`, `403` `not_local`, `404` `agent_not_enrolled`, `413` `definition_too_large`, `422` `definition_invalid`, `definition_contains_secret` or `schema_version_unsupported`, or `500` `internal_error` if the definition couldn't be stored or read.

## Model requests

Model requests go to the same proxy address, at the provider's own paths, such as `/v1/chat/completions` for OpenAI or `/v1/messages` for Anthropic. A proxy fronts one provider's gateway endpoint, so an agent that calls both runs through two proxies ([One proxy per provider](../guides/run-an-agent.mdx#one-proxy-per-provider)). Send your provider key in the provider's usual header (`Authorization` for OpenAI, `x-api-key` for Anthropic): the proxy passes it on unchanged.

Three headers name who's asking:

- **`X-Viper-Agent`:** the enrolled agent. The request is signed with its key.
- **`X-Viper-Definition`:** the `definitionCid` its registration returned. Required with `X-Viper-Agent`: without it, the request is `409` `definition_unknown`.
- **`X-Viper-Subagent`:** for a sub-agent, its path in the definition, such as `critic` or `critic/fact-checker`. The request is signed as the agent, and the sub-agent is recorded.

Each request is signed as matching the definition it names, so send what the definition records: its prompts as the system or developer messages, its tools, and its model and settings. The proxy doesn't compare them ([Send what the definition says](../guides/run-an-agent.mdx#send-what-the-definition-says)).

The proxy checks the headers before signing, and refuses rather than sign as someone else:

- **Each header once:** a repeated, empty or unreadable header is `400` `agent_header_invalid`.
- **The agent must be enrolled** (`409` `agent_not_enrolled`), **its definition registered** (`409` `definition_unknown`), and **the sub-agent declared** in it (`409` `subagent_unknown`).
- **No definition or sub-agent without an agent:** `403` `agent_header_required`.
- **No requests from web pages:** a request naming an agent with an `Origin` header is `403` `not_local`.
- **A request naming no agent** is signed as the proxy itself, unless the proxy runs with `--require-agent-header`, which refuses it with `403` `agent_header_required`.

The proxy removes the three headers before forwarding the request. Errors on the way are `signing_failed`, `upstream_unreachable`, `upstream_response_lost`, `request_unreadable` and `internal_error` ([Model requests](./errors.mdx#model-requests)). Refusals from the Guardian Platform and the provider are passed on unchanged, without `X-Viper-Error`.

## Retrying

Every error the proxy makes with a code carries an `x-should-retry` header: `true` for the codes it's safe to retry, and `false` for the rest. Clients that follow it, such as OpenAI's and Anthropic's generated SDKs in Python, TypeScript, Go and Java, don't retry what they shouldn't; without it they retry every `409` and `5xx`. Check that yours reads it. A client of your own should follow the header, or branch on `code`:

- **Safe to retry:** `signing_failed`, `internal_error` and `upstream_unreachable`. Nothing reached the model.
- **Never retry `upstream_response_lost`:** the request was sent and the model may have run, so a retry may run it twice.
- **Don't retry the rest as they are:** `4xx` codes need something fixed first. `definition_unknown` needs the definition registered again ([below](#after-a-restart)).

A few errors carry no `x-should-retry`, since the proxy can't tell what caused them: one for a request naming no agent that the proxy couldn't sign, and one raised before the agent headers are read (relay-mode dispatch, HTTPS interception). The Python SDK marks each refusal the same way itself, for proxies older than the header ([Retries](./errors.mdx#retries)).

## After a restart

A restarted proxy has forgotten every registration, so the next model request is `409` `definition_unknown`, with `X-Viper-Error: 1`. Nothing reached the model. Recover as the Python SDK does:

1. `POST` the same definition to `/__viper__/v1/agents/{agent}/definitions`. It gets the same `definitionCid` back.
2. Send the refused request once more.
3. If that's refused with `definition_unknown` again, stop and report it: something else is wrong.

Files stored through the collections endpoint persist across restarts.

## A whole exchange

With the proxy at `http://127.0.0.1:8080` and the agent `researcher` enrolled:

```sh
# 1. Check the proxy
curl -s http://127.0.0.1:8080/__viper__/v1/capabilities

# 2. Store a skill's files, if it has any; keep the cid
curl -s http://127.0.0.1:8080/__viper__/v1/collections \
  -H 'Content-Type: application/json' \
  -d "{\"files\": {\"SKILL.md\": \"$(base64 < skills/triage/SKILL.md | tr -d '\n')\"}}"

# 3. Register the definition, and keep its definitionCid
curl -s -X POST http://127.0.0.1:8080/__viper__/v1/agents/researcher/definitions \
  -H 'Content-Type: application/json' \
  -d '{"schema": "viper/agent-definition/v1", "name": "researcher",
       "framework": {"name": "custom"},
       "models": [{"provider": "openai", "model": "gpt-4.1"}]}'

# 4. Make model requests as the agent
curl -s http://127.0.0.1:8080/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'X-Viper-Agent: researcher' \
  -H "X-Viper-Definition: $DEFINITION_CID" \
  -d '{"model": "gpt-4.1", "messages": [{"role": "user", "content": "Hello"}]}'
```

Viper itself is installed and configured as the Guardian Platform docs describe ([Install Viper](https://guardian.docs.eqtylab.io/user-guide/install-viper/)).