# Messages

Create a message with the Anthropic Messages format, and estimate how many input tokens a request will use. The endpoints are compatible with the Anthropic API, so the official Anthropic SDKs and Claude Code work once you change the base URL.

`POST /v1/messages`

`POST /v1/messages/count_tokens`

Use `https://kenari.id` as the base URL, without `/v1`. The Anthropic SDKs append `/v1/messages` themselves, and a base URL of `https://kenari.id/v1` produces `/v1/v1/messages`, which returns 405. For Claude Code setup, see [Claude Code](/en/docs/claude-code).

Authenticate with `x-api-key: kn-...` or `Authorization: Bearer kn-...`. See [Authentication and API keys](/en/docs/authentication).

## Request

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | A catalog id, a `:free` id, a route name or a `byok/` id. The same values as in [Chat completions](/en/docs/chat#choose-a-model). |
| `messages` | array | Yes | The conversation. See [Messages array](#messages-array). |
| `max_tokens` | integer | No | Upper limit on generated tokens. Optional here, unlike the Anthropic API. Default 65,536. |
| `system` | string or array | No | System prompt, as a string or an array of `{"type": "text", "text": "..."}` blocks. Text blocks are joined with a blank line, and other block types are ignored. |
| `stream` | boolean | No | Set `true` to receive Server-Sent Events. See [Streaming](/en/docs/streaming). |
| `temperature` | number | No | Sampling temperature. |
| `top_p` | number | No | Nucleus sampling. |
| `top_k` | integer | No | Sample from the top K tokens. |
| `stop_sequences` | array | No | Strings that end generation. |
| `tools` | array | No | Function tools with `name`, `description` and `input_schema`, and kenari server tools such as `{"type": "kenari:web_search"}`. See [Function calling](/en/docs/function-calling) and [Server tools](/en/docs/server-tools). |
| `tool_choice` | object | No | `{"type": "auto"}`, `{"type": "any"}`, `{"type": "none"}` or `{"type": "tool", "name": "..."}`. |
| `thinking` | object | No | `{"type": "enabled", "budget_tokens": 4096}` or `{"type": "disabled"}`. See [Reasoning](/en/docs/reasoning). |
| `output_config` | object | No | `{"effort": "high"}` sets a reasoning level without a token budget. See [Reasoning](/en/docs/reasoning). `{"format": {"type": "json_schema", "schema": {...}}}` asks for JSON that matches a schema. See [Structured output](/en/docs/structured-output). |
| `output_format` | object | No | Older spelling of `output_config.format`, accepted with the same meaning. `output_config.format` wins when both are sent. |
| `metadata` | object | No | Accepted. |

kenari does not interpret other fields. It passes them to the provider unchanged when the model is served in the Anthropic format and drops them otherwise, so do not depend on them.

The routing fields `provider`, `models` and `route` are the exception. kenari passes them on only when the request runs on your own key, such as a BYOK id. On every other model it does not pass them on, so they have no effect. `plugins` is not used on this endpoint and is never passed on.

### Messages array

Each message has a `role` and a `content` field. `role` is `user` or `assistant`. A `system` message is also accepted in the array, which newer Claude Code versions send. `content` is a string or an array of blocks.

| Block | Use |
| --- | --- |
| `text` | Plain text. |
| `image` | An image from a `base64` or `url` source. See [Images and files as input](/en/docs/vision). |
| `document` | A PDF from a `base64` source is read on any model, the same way as a chat `file` part, and billed as input tokens. A `url` source reaches only models served through an Anthropic-style API and is dropped without an error for other models. A `text` source is plain text. See [Read documents](/en/docs/documents). |
| `tool_use` | A function call the model made earlier. |
| `tool_result` | The result of a call. `content` is a string or an array of `text` and `image` blocks. Set `is_error` to report a failure. |
| `thinking`, `redacted_thinking` | Earlier reasoning, sent back unchanged. |

Unknown block types are ignored. A block of a known type that is malformed returns 400.

### Prompt caching

Send `cache_control` markers as you would to Anthropic. kenari keeps them when the model is served in the Anthropic format. It also adds its own cache breakpoints when it rebuilds the request, so caching does not depend on your markers. See [Prompt caching](/en/docs/prompt-caching).

## Response

The response is an Anthropic `message` object.

```json
{
  "id": "4f1c9a2e7b3d5a60",
  "type": "message",
  "role": "assistant",
  "model": "step-3-7-flash:free",
  "content": [
    { "type": "text", "text": "Hello! How can I help?" }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": { "input_tokens": 9, "output_tokens": 12 }
}
```

| Field | Description |
| --- | --- |
| `content` | An array of blocks: `text`, `tool_use`, and `thinking` when the model reasoned. See [Reasoning](/en/docs/reasoning). |
| `stop_reason` | `end_turn`, `max_tokens`, `stop_sequence`, `tool_use` or `refusal`. |
| `stop_sequence` | Always `null`. When a stop sequence ends the message, `stop_reason` is `stop_sequence`. |
| `usage.input_tokens` | Input tokens that were not served from or written to the cache. |
| `usage.output_tokens` | Output tokens. Reasoning tokens are billed as output. |
| `usage.cache_read_input_tokens` | Input tokens read from cache. Present when the provider reports it. |
| `usage.cache_creation_input_tokens` | Input tokens written to cache. Present when the provider reports it. |
| `sources` | Present when a kenari server tool searched the web. Lists `{url, title}` for each source. |

## Count tokens

`POST /v1/messages/count_tokens` takes the same body as `/v1/messages` and returns an estimate of the input tokens.

```json
{ "input_tokens": 41 }
```

The number is estimated from the size of the request, not counted with a tokenizer, so treat it as a rough guide for fitting a context window. The call needs a valid key but does not check the `model`, does not run it and costs nothing. A body that cannot be parsed returns 400.

```bash
curl https://kenari.id/v1/messages/count_tokens \
  -H "x-api-key: $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "step-3-7-flash:free",
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

## Examples

### curl

```bash
curl https://kenari.id/v1/messages \
  -H "x-api-key: $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "step-3-7-flash:free",
    "max_tokens": 512,
    "system": "You answer briefly.",
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

### Python

```python
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://kenari.id",
    api_key=os.environ["KENARI_API_KEY"],
)

message = client.messages.create(
    model="step-3-7-flash:free",
    max_tokens=512,
    system="You answer briefly.",
    messages=[{"role": "user", "content": "Hello!"}],
)
print("".join(block.text for block in message.content if block.type == "text"))
```

### TypeScript

```ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://kenari.id",
  apiKey: process.env.KENARI_API_KEY,
});

const message = await client.messages.create({
  model: "step-3-7-flash:free",
  max_tokens: 512,
  system: "You answer briefly.",
  messages: [{ role: "user", content: "Hello!" }],
});
for (const block of message.content) {
  if (block.type === "text") console.log(block.text);
}
```

## Billing

Each message is billed per token at the model's Input, Output, Cache read and Cache write prices. `:free` models cost nothing. `count_tokens` is free. See [How billing works](/en/docs/billing).

## Errors

Errors use the Anthropic envelope: an object with `"type": "error"` and an `error` object that holds `type` and `message`. Only the errors specific to this endpoint are listed here. The rest are in [Errors](/en/docs/errors).

| Status | `error.type` | When |
| --- | --- | --- |
| 400 | `invalid_request_error` | The body is not valid JSON or lacks `model` or `messages`. A role, a `tool_choice` type or a known block type is malformed. A `byok/` id is malformed. The model is not a chat model, such as an image or speech model. The request is longer than the model's context window. |
| 400 | `not_found_error` | The `model` is not in the catalog and is not one of your routes, or you saved no key for the provider in a `byok/` id. |
| 403 | `permission_error` | The model is only for accounts with an active subscription. A `byok/` id names a provider whose keys cannot serve right now. The API key has a spend cap that BYOK requests cannot use. |
| 405 | none | The base URL ends in `/v1`, so the SDK called `/v1/v1/messages`. Use `https://kenari.id`. |

## See also

- [Streaming](/en/docs/streaming)
- [Reasoning](/en/docs/reasoning)
- [Chat completions](/en/docs/chat)
- [Claude Code](/en/docs/claude-code)
