The Code API
AETHER Coder talks to exactly one endpoint on your platform. This article documents it — for network teams who need to allowlist it, and for anyone wiring a different client to the same agent.
Nothing here is required for normal use of the extension. If you are setting up your own machine, Connect to AETHER is the article you want.
The endpoint
POST /api/code/v1/chat/completions
Content-Type: application/json
The request and response follow the OpenAI chat-completions shape, so an OpenAI-compatible
client library can be pointed at it by changing the base URL and the credentials. The base
URL is your platform host plus /api/code/v1, for example
https://your-aether-host/api/code/v1.
Send "model": "aether". It is a fixed identifier rather than a model name — the real
model belongs to the agent and is resolved on the platform, from the agent's configuration.
The endpoint is stateless. The client owns the transcript and sends the whole message list on every request; the platform keeps no conversation to resume.
Two credentials, both required
Every request carries two headers, and a request missing either one is refused:
| Header | Value | What it identifies |
|---|---|---|
Authorization |
Bearer <agent API key> |
Which agent answers. Taken from the agent's published page. |
X-Aether-Access-Token |
aeth_… |
Which person is asking. Generated under My Account → Access Tokens. |
The access token is what makes a coding session auditable: the recorded conversation is attributed to the token's owner rather than to a shared machine account. See Access Tokens.
Warning: the agent API key spends that agent's budget, so treat it as a shared secret — server-side only, never in client-side code or a public repository. The same guidance applies as for the rest of the platform API; see Authentication.
A minimal request
curl -N https://your-aether-host/api/code/v1/chat/completions \
-H "Authorization: Bearer YOUR_AGENT_API_KEY" \
-H "X-Aether-Access-Token: aeth_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "aether",
"stream": true,
"messages": [
{ "role": "user", "content": "Summarise what this service does." }
]
}'
The response
With "stream": true the answer arrives as server-sent events: the response has
content type text/event-stream, each event is a data: line carrying one
OpenAI-style chunk, and the stream finishes with a final data: [DONE].
data: {"choices":[{"delta":{"content":"This service "}}]}
data: {"choices":[{"delta":{"content":"handles orders."}}]}
data: [DONE]
With "stream": false the same answer comes back as a single JSON completion object
instead. Streaming is what the extension uses, because it is what makes a long reply feel
responsive in an editor.
What happens on the platform
A great deal of what the agent can do is not configured in the client at all:
- Knowledge-base search runs server-side while the agent answers.
- The platform's MCP tools attached to the agent run server-side too.
- Budgets, model choice, rates and the audit trail are all applied on the platform.
A non-editor client therefore configures none of those things — it sends messages and reads the reply. See Knowledge & Tools for how this looks from the editor, and Connecting MCP Tools for attaching tools to an agent.
What to allowlist
On a restricted network, one host and one path prefix carry the agent connection itself:
| Allow | Value |
|---|---|
| Host | Your platform host, for example your-aether-host |
| Path prefix | /api/code/v1 |
| Protocol | HTTPS |
Three features reach other hosts by design, so allowlist them separately if your developers
need them: @url mentions fetch whichever page you name, browser use loads whatever
address the task requires, and an MCP server you add in the editor — including one attached
from the Remote Servers tab — connects to its own endpoint.
Two practical notes for whoever configures the proxy:
- Responses are streamed and long-lived. An intermediary that buffers responses or applies a short idle timeout will make the agent appear to hang part-way through a reply.
- The extension sends nothing to any third-party AI service. Your code, prompts and diffs travel to the host you configured and nowhere else.
Errors
Errors come back as a JSON body of this shape, streamed or not:
{ "error": { "message": "Access token is invalid, expired or revoked.", "type": "aether_error" } }
| Status | Meaning |
|---|---|
401 |
A credential is missing or invalid — either the agent API key or the access token, including an expired or revoked token. |
403 |
The agent is not in Code Agent mode. See Enabling Code Agent Mode. |
400 |
The request could not be completed — a malformed body, or a platform check that refused it. |
A message refused by the content guards carries an extra code so a client can tell it apart
from a transient failure and stop retrying:
{ "error": { "message": "This message contains personal information (1 South African ID) and this agent is configured to block it. Remove it, or ask an administrator to change the agent's PII policy.", "type": "aether_error", "code": "PII_BLOCKED_ERROR", "details": [] } }
The same code is used whether the block came from
PII Guard or Secrets Guard; the message
says which. The outcome is determined by the text, so retrying an identical request always
fails the same way. details carries the values the author may allowlist, and is present but
empty for a credential block — a credential can never be allowlisted.
An exhausted budget falls into that last category. The check runs before the request is served, so the failure arrives as an error with an explanatory message rather than as a truncated answer, and nothing is left half-written. See Cost, Budgets & Auditing.
For the rest of the platform's API — conversations, history and the agent endpoints the extension does not use — see REST Endpoints.
The PII allowlist
Three endpoints manage the values a named person has declared are not personal data for this agent, under the same base URL and the same two credentials as a chat request. The access token is what attributes an entry to a person, so both are required.
| Call | Result |
|---|---|
GET /allowlist |
{ "entries": [ { "id", "value", "kind", "created_by", "created_at" } ] } for the agent named by the API key. |
POST /allowlist |
Body { "entries": [ { "kind", "value" } ] }. Returns only the batch you posted, not the whole list — call GET for that. Adding the same value twice is harmless. |
DELETE /allowlist/{id} |
204. |
Refusals come back as 400 with a message that says why: a credential kind, a value over 256
characters, or an agent already at its 500-entry ceiling. An agent in another organisation
answers 404 — not 403 — because the caller has no allowlist there to speak of.
See PII Guard for what an entry does and where entries are reviewed.
Next steps
- Connect to AETHER — the same values, entered in the extension.
- REST Endpoints — the rest of the platform API.
- Troubleshooting — what these errors look like in the editor.