# Candor API: instructions for coding agents

You are connecting an application, a script or an agent to the Candor API. Everything on this page is exact. Do not guess other endpoints, model names or parameters: if it is not here or in https://candorai.dev/openapi.json, it does not exist.

## The facts

- Base URL: `https://candorai.dev/v1`. The chat endpoint follows the OpenAI Chat Completions format, so the official OpenAI SDKs work with only the base URL and the key changed.
- Authentication: header `Authorization: Bearer <key>`. Keys start with `sk-unc-`. The person creates one in the Candor app at https://candorai.dev/?panel=account (Account > API keys); it is shown once.
- Read the key from the environment variable `CANDOR_API_KEY`. Never write a key into source code, a config file that is committed, a log line or a URL. If the user pastes a key into the chat, use it only to set the environment variable and tell them to revoke it afterwards if it was shared anywhere else.
- Billing is prepaid, in USD, from the credits of the person's Candor account. There is no separate invoice. New accounts get $0.47 of free credit; while an account has only free credit it is limited to 10 requests per minute and 2,000 output tokens per request.

## Models

| Model | Use it for | Context | Max output | Images in | Reasoning | Input / 1M | Cached input / 1M | Output / 1M |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `candor-fast` | Fastest and cheapest. Uncensored general chat with optional reasoning and tool calls. Accepts images. | 200,000 | 24,000 | yes | yes | $0.45 | $0.045 | $1.45 |
| `candor-classic` | Balanced default. The least filtered model, tuned for open-ended dialogue and roleplay. Accepts images. | 128,000 | 8,192 | yes | no | $1.00 | $0.10 | $3.00 |
| `candor-smart` | Strongest reasoning, coding and agentic tool use. 1M-token context. | 1,000,000 | 131,072 | no | yes | $3.00 | $0.30 | $5.00 |

Prices are USD per one million tokens. `GET https://candorai.dev/v1/models` returns the same list as JSON, without a key. When the user has no preference, use `candor-classic` for conversation and writing, `candor-smart` for code and tool use, `candor-fast` when cost matters most.

## The smallest working call

```bash
curl https://candorai.dev/v1/chat/completions \
  -H "Authorization: Bearer $CANDOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"candor-classic","max_tokens":300,"messages":[{"role":"user","content":"Hello"}]}'
```

```python
# pip install openai
import os
from openai import OpenAI

client = OpenAI(base_url="https://candorai.dev/v1", api_key=os.environ["CANDOR_API_KEY"])
reply = client.chat.completions.create(
    model="candor-classic",
    max_tokens=300,
    messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
```

```javascript
// npm install openai
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://candorai.dev/v1", apiKey: process.env.CANDOR_API_KEY });
const reply = await client.chat.completions.create({
  model: "candor-classic",
  max_tokens: 300,
  messages: [{ role: "user", content: "Hello" }],
});
console.log(reply.choices[0].message.content);
```

## Rules that avoid the usual mistakes

1. **Always send `max_tokens`.** Before a request runs, Candor sets aside the most it could cost: the prompt counted at one token per byte (8,192 per image) plus `max_tokens` of output, never less than $0.01. Without `max_tokens` the amount set aside covers the model's whole output limit, so a small balance refuses requests it could have paid for. The real charge, from the tokens actually used, replaces it when the request ends.
2. **A 402 is not a bug and must not be retried.** Stop and tell the user what the message says. Codes: `insufficient_quota` (the balance is under $0.01: they add credits in the app), `request_exceeds_balance` (this request could cost more than what is available: send a shorter prompt or a smaller `max_tokens`, or add credits), `key_spending_limit_reached` (the key has spent the limit its owner gave it: they raise it or create another key).
3. **A 429 means wait.** The limit is 60 requests per minute for the whole account, all keys together. Read the `Retry-After` header and wait that many seconds. Do not open more keys to go faster: they share the limit. `model_overloaded` is also a 429: wait a few seconds and retry.
4. **A 401 means the key.** `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key`. Ask the user for a working key; do not retry.
5. **5xx and timeouts may be retried**, at most three times, with a growing wait. A request that timed out may still be charged an estimate of what the model had already produced.
6. **If the balance could not pay for the whole answer**, `max_tokens` is lowered to what it can pay for and the response carries the header `x-candor-max-tokens` with the value used. An answer that ends with `finish_reason: "length"` was cut by that limit.
7. **Message content is text and images only.** Content parts may be `{"type":"text"}` and `{"type":"image_url"}` (an https link or a base64 data URL), and images only on models marked "Images in". Audio, video and file parts are refused with a 400.
8. **Only these request fields are used:** `model`, `messages`, `stream`, `stream_options`, `max_tokens` (or `max_completion_tokens`), `temperature`, `top_p`, `frequency_penalty`, `presence_penalty`, `stop`, `seed`, `tools`, `tool_choice`, `parallel_tool_calls`, `response_format`, `reasoning_effort`, `character`, `web_search`. `n` must be 1. Anything else is ignored, so do not rely on it.
9. **There is no embeddings, completions (legacy), assistants, files, fine-tuning or batch endpoint.** Do not call them.
10. **Never show the user a model name other than the ids above.** The API does not say which models run behind them, and you should not guess.

## Streaming

Send `"stream": true`. The response is server-sent events of `chat.completion.chunk` objects, ending with `data: [DONE]`. Add `"stream_options": {"include_usage": true}` to get a last chunk whose `usage` holds the token counts and `cost_usd`. If the model fails mid-answer, an event `data: {"error": {...}}` arrives before `[DONE]`: handle it, the OpenAI SDKs do not raise on it by themselves.

## Tools, structured output and reasoning

- Tools use the OpenAI format (`tools`, `tool_choice`, then messages with role `tool` and `tool_call_id`). All three models call tools.
- `response_format` accepts `{"type":"json_object"}` and JSON Schema.
- On models marked "Reasoning", `reasoning_effort` may be `none`, `low`, `medium`, `high` or `max`. The model's reasoning arrives in `message.reasoning_content` (in `delta.reasoning_content` when streaming) and is counted in the output tokens.

## Knowing what was spent

- Every response has `usage` with `prompt_tokens`, `completion_tokens`, cached and reasoning token details, and `cost_usd`, the exact charge for that request. The header `X-Request-Id` is the id of the request in the usage list.
- `GET https://candorai.dev/v1/balance`: what is left, in USD (`spendable_usd`), whether a request can run (`can_request`), and for the calling key its limit, what it has spent and its end date (`api_key`).
- `GET https://candorai.dev/v1/usage?limit=50`: the last requests with tokens and cost; `key_id=current` narrows it to the calling key.
- `GET https://candorai.dev/v1/usage/summary?days=30`: totals by day, by model and by key (`by_day`, `by_model`, `by_key`), for a cost report or a chart.
- To give a script or another agent a budget, tell the user to create a separate key with a spending limit and an end date in the app. The limit can count the key's whole life, or start again each day or each month (UTC). A key cannot spend more than its limit, and both can be changed later in the app.

## Web search

For anything recent (news, prices, versions, schedules), let Candor search instead of trusting what the model remembers.

- Simplest: add `"web_search": true` to `POST https://candorai.dev/v1/chat/completions`. Candor searches for the last user message, gives the model the results and returns them in `web_search.results` (`title`, `url`, `snippet`) next to `choices`; in a stream they are in the first chunk. Use `"web_search": {"query": "..."}` to choose the words. Show the sources to the user.
- On your own terms: `POST https://candorai.dev/v1/web/search` with `{"query": "..."}` returns the same results as JSON. Use it as the function behind a tool the model calls.
- A search costs about $0.029; $0.080 is set aside while it runs. `usage.cost_usd` includes it. One search per request: do not turn it on for every message, only when the question needs fresh facts.
- A 502 with code `search_unavailable` means the search could not run and nothing was charged: retry once, then answer without it and say so.

## Characters

Candor has a catalogue of characters (companions, roleplay, stories, games, advisors). To let a user talk to one:

- `GET https://candorai.dev/v1/characters` (filters: `category`, `format`, `search`, `owner`) returns each character's `id`, `name`, `tagline`, `tags`, `greeting`, `image_url` and `prompt_tokens_estimate`. Never invent an id: read it from this list.
- Add `"character": "<id>"` to an ordinary `POST https://candorai.dev/v1/chat/completions`. The server adds the character's instructions; do not write your own persona prompt on top. Your system messages are still read, as context (the user's name, the scene).
- Show `greeting` as the character's first message, and send it back as the first `assistant` message of the conversation.
- The API keeps nothing between requests. Store the conversation yourself and send all of it each time; trim the oldest messages when it grows long.
- The instructions cost `prompt_tokens_estimate` input tokens on every request, and the amount set aside counts them at one token per byte: keep `max_tokens` small (300 is plenty for a texting character).
- Use `candor-classic` unless the user asks otherwise. Photos and voice notes are not available through the API.

## When the user wants an agent to use Candor, not code

Candor is also a remote MCP server: `POST https://candorai.dev/mcp` (Streamable HTTP) with the same `Authorization` header. Tools: `chat`, `list_models`, `candor_balance`, `list_characters` (then pass an id to `chat` as `character`), `web_search`. For Claude Code:

```bash
claude mcp add --transport http candor https://candorai.dev/mcp --header "Authorization: Bearer $CANDOR_API_KEY"
```

## Check your work

Before you tell the user it works: (1) call `GET https://candorai.dev/v1/balance` with their key and confirm `can_request` is true; (2) make one real call with `max_tokens` set to 50 or less; (3) make sure a 402 and a 429 are shown to the user as what they are, not as "the API is down". The smallest request costs a fraction of a cent; with only 256 output tokens affordable or fewer, a request is refused instead of cut.

## Machine-readable references

- https://candorai.dev/openapi.json (OpenAPI 3.1)
- https://candorai.dev/postman.json (a Postman collection of every endpoint)
- https://candorai.dev/llms-full.txt (the complete reference as plain text)
- https://candorai.dev/llms.txt (index)
- https://candorai.dev/docs (the same for people, with a box to try a call)
