# Create a message (Anthropic format)

Source: https://router.africa/docs/api-reference/create-message

`POST https://api.router.africa/v1/messages`

Anthropic Messages-compatible endpoint: point the Anthropic SDK's base URL
at `https://api.router.africa` and keep your code. Set `stream: true` to
receive Anthropic-style server-sent events.

Authenticate with `x-api-key: YOUR_KEY` (what the Anthropic SDK sends) or
`Authorization: Bearer YOUR_KEY`. This is the only endpoint that accepts
`x-api-key`.

Errors use Anthropic's error format, not the envelope of the other
endpoints: `{"type": "error", "error": {"type": "...", "message": "...",
"reason": "..."}}`. `error.type` is Anthropic's type; `error.reason` is
our specific cause (see the Errors page). An error before the first token
is an ordinary HTTP error. An error after streaming has started cannot
change the `200`; it is sent as a final `event: error` event and the
stream ends.

Not supported: a `fallbacks` array and `models` (fallback chains are
configured by router.africa, not per request).

## Authentication

Send your key as `Authorization: Bearer YOUR_KEY`. Never put a key in the URL or the body: the request is refused with `key_not_in_header`.

## Parameters

- `Authorization` (header, string, required): Bearer YOUR_KEY.
- `Idempotency-Key` (header, string): router.africa extension. Works exactly as on `POST /v1/chat/completions`: a repeat of a finished request with the same key and the same API key returns the first result (marked `x-idempotent-replay: true`, not charged again); a repeat while the first is still running returns `409` `request_in_progress`. Without this header an identical body is treated the same way.

## Request body

- `model` (string, required): A model id from `GET /v1/models`.
- `messages` (object[], required): Min items: 1
  - `role` ("user" | "assistant", required)
  - `content` (string | MessageContentBlock[], required): Text, or an array of content blocks.
    - `variant 1` (MessageContentBlock[])
      - `type` (string, required)
      - `text` (string)
      - `cache_control` (object)
- `max_tokens` (integer, required): Output cap. A value above the model's ceiling is silently lowered to the ceiling; it is not an error. Greater than 0
- `system` (string | MessageContentBlock[]): System prompt, as text or an array of content blocks.
  - `variant 1` (MessageContentBlock[])
    - `type` (string, required)
    - `text` (string)
    - `cache_control` (object)
- `stream` (boolean): Default: `false`
- `temperature` (number)
- `top_p` (number)
- `top_k` (integer)
- `stop_sequences` (string[])
- `tools` (object[])
- `tool_choice` (object)
- `thinking` (object): Extended thinking settings, as in Anthropic's API.
- `context_management` (object)
- `metadata` (object)

## Example request

**curl**

```bash
curl https://api.router.africa/v1/messages \
  -H "Authorization: Bearer $ROUTER_AFRICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-4.5",
    "max_tokens": 100,
    "messages": [
      {
        "role": "user",
        "content": "Habari! Say hello in Swahili."
      }
    ]
  }'
```

**Python**

```python
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.router.africa",
    api_key=os.environ["ROUTER_AFRICA_API_KEY"],  # sent as x-api-key
)

message = client.messages.create(
    model="anthropic/claude-sonnet-4.5",
    max_tokens=100,
    messages=[{"role": "user", "content": "Habari! Say hello in Swahili."}],
)
print(message.content[0].text)
```

**TypeScript**

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

const client = new Anthropic({
  baseURL: "https://api.router.africa",
  apiKey: process.env.ROUTER_AFRICA_API_KEY, // sent as x-api-key
});

const message = await client.messages.create({
  model: "anthropic/claude-sonnet-4.5",
  max_tokens: 100,
  messages: [{ role: "user", content: "Habari! Say hello in Swahili." }],
});
console.log(message.content[0]);
```

## Response

The message, or a stream of events when `stream` is true.

- `id` (string, required)
- `type` ("message", required)
- `role` ("assistant", required)
- `model` (string, required)
- `content` (MessageContentBlock[], required)
  - `type` (string, required)
  - `text` (string)
  - `cache_control` (object)
- `stop_reason` (string | null, required)
- `stop_sequence` (string | null)
- `usage` (MessageUsage, required)
  - `input_tokens` (integer, required)
  - `output_tokens` (integer, required)
  - `cache_creation_input_tokens` (integer | null)
  - `cache_read_input_tokens` (integer | null)

```json
{
  "id": "msg_0192f0e0",
  "type": "message",
  "role": "assistant",
  "model": "anthropic/claude-sonnet-4.5",
  "content": [
    {
      "type": "text",
      "text": "Habari!"
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 12,
    "output_tokens": 3
  }
}
```

## Streaming

Server-sent events in Anthropic's format: each event has an
`event:` line naming its type and a `data:` line with one JSON
`MessageStreamEvent` (or, last, an `event: error` event carrying an `AnthropicError`). There is no
`[DONE]` line: the stream ends after `message_stop`.

Event schema `MessageStreamEvent`:

- `type` ("message_start" | "content_block_start" | "content_block_delta" | "content_block_stop" | "message_delta" | "message_stop" | "ping", required)
- `index` (integer)
- `message` (Message)
  - `id` (string, required)
  - `type` ("message", required)
  - `role` ("assistant", required)
  - `model` (string, required)
  - `content` (MessageContentBlock[], required)
    - `type` (string, required)
    - `text` (string)
    - `cache_control` (object)
  - `stop_reason` (string | null, required)
  - `stop_sequence` (string | null)
  - `usage` (MessageUsage, required)
    - `input_tokens` (integer, required)
    - `output_tokens` (integer, required)
    - `cache_creation_input_tokens` (integer | null)
    - `cache_read_input_tokens` (integer | null)
- `content_block` (MessageContentBlock): A content block. `type` is for example `text`, `image`, `tool_use`, `tool_result` or `thinking`. `cache_control` may be set on a block. 
  - `type` (string, required)
  - `text` (string)
  - `cache_control` (object)
- `delta` (object)
- `usage` (MessageUsage)
  - `input_tokens` (integer, required)
  - `output_tokens` (integer, required)
  - `cache_creation_input_tokens` (integer | null)
  - `cache_read_input_tokens` (integer | null)

Event schema `AnthropicError`:

- `type` ("error", required)
- `error` (object, required)
  - `type` ("invalid_request_error" | "authentication_error" | "billing_error" | "permission_error" | "not_found_error" | "rate_limit_error" | "api_error" | "overloaded_error", required): Anthropic's error type, from the HTTP status.
  - `message` (string, required): Safe to show to a user.
  - `reason` ("invalid_body" | "key_not_in_header" | "context_length_exceeded" | "missing_api_key" | "invalid_api_key" | "insufficient_credits" | "key_spend_cap_exceeded" | "account_suspended" | "content_policy_violation" | "model_not_found" | "route_not_found" | "upstream_timeout" | "request_in_progress" | "payload_too_large" | "rate_limited" | "concurrency_limited" | "edge_rate_limited" | "internal_error" | "provider_error" | "service_unavailable" | "provider_overloaded", required): Our specific cause, the same values as `metadata.reason` in the standard error.
  - `model` (string): On `model_not_found`, the model id that was sent.

```text
event: message_start
data: {"type":"message_start","message":{"id":"msg_0192f0e0","type":"message","role":"assistant","model":"anthropic/claude-sonnet-4.5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":12,"output_tokens":1}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Habari!"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":3}}

event: message_stop
data: {"type":"message_stop"}

(an error after streaming has started instead ends the stream with:)
event: error
data: {"type":"error","error":{"type":"overloaded_error","message":"The model provider is overloaded. Retry shortly.","reason":"provider_overloaded"}}

```

## Errors

### 400

Same causes as the standard 400 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "Request body is invalid: `messages` is required.",
    "reason": "invalid_body"
  }
}
```

### 401

Same causes as the standard 401 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "You didn't provide an API key. Send it as `x-api-key: YOUR_KEY` or `Authorization: Bearer YOUR_KEY`.",
    "reason": "missing_api_key"
  }
}
```

### 402

Same causes as the standard 402 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "billing_error",
    "message": "This workspace doesn't have enough credits for this request. Add credits at https://app.router.africa.",
    "reason": "insufficient_credits"
  }
}
```

### 403

Same causes as the standard 403 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "permission_error",
    "message": "This account is suspended. Contact support.",
    "reason": "account_suspended"
  }
}
```

### 404

Same causes as the standard 404 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "not_found_error",
    "message": "The model `anthropic/unknown` does not exist or you do not have access to it.",
    "reason": "model_not_found"
  }
}
```

### 408

Same causes as the standard 408 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "api_error",
    "message": "The model provider took too long to respond.",
    "reason": "upstream_timeout"
  }
}
```

### 409

Same causes as the standard 409 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "An identical request is already in progress.",
    "reason": "request_in_progress"
  }
}
```

### 413

Same causes as the standard 413 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "The request body is too large. Send less than 16 MiB.",
    "reason": "payload_too_large"
  }
}
```

### 429

Same causes as the standard 429 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "rate_limit_error",
    "message": "Rate limit reached for this API key. Retry after 5 seconds.",
    "reason": "rate_limited"
  }
}
```

### 500

Same causes as the standard 500 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "api_error",
    "message": "Something went wrong on our side. Reference: 0192f0e0-aaaa-7000-8000-000000000000.",
    "reason": "internal_error"
  }
}
```

### 502

Same causes as the standard 502 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "api_error",
    "message": "The model provider returned an error.",
    "reason": "provider_error"
  }
}
```

### 503

Same causes as the standard 503 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "api_error",
    "message": "The service is temporarily unavailable.",
    "reason": "service_unavailable"
  }
}
```

### 529

Same causes as the standard 529 response, in Anthropic's error format.

```json
{
  "type": "error",
  "error": {
    "type": "overloaded_error",
    "message": "The model provider is overloaded. Retry shortly.",
    "reason": "provider_overloaded"
  }
}
```
