---
title: "Create message"
description: "Create a message (Anthropic Messages protocol)."
canonical: "https://pirouter.ai/docs/api/messages"
last-updated: "2026-10-13"
lang: "en"
---

# Create message

Create a message (Anthropic Messages protocol).

`POST https://api.pirouter.ai/v1/messages`

Anthropic-compatible Messages API. A model is served only on the protocols it is registered for; calling one that does not accept this protocol is `validation_failed` (422). With `"stream": true` the response is a Server-Sent Events stream of named message events; otherwise a single JSON body. Only the fields listed in the request schema are read by the platform; every other field (`system`, `tools`, ...) is forwarded to the upstream provider as-is. SDKs of this protocol send the key in `X-Api-Key`, which is accepted as-is.

## Authorization

Send your API key in one of these headers.

| Header | Value | Description |
| --- | --- | --- |
| `Authorization` | `Bearer sk-pi-…` | Platform-issued API key with the `sk-pi-` prefix, sent as a Bearer token. |
| `X-Api-Key` | `sk-pi-…` | The same platform-issued `sk-pi-` key, sent in `X-Api-Key`. Read only when `Authorization` is absent or empty; when both are sent, `Authorization` wins. A bare key without the `Bearer ` prefix is accepted in either header. |

## Body

`application/json`

Anthropic Messages request. Only the fields the platform reads or the protocol requires are listed; the rest of the protocol's fields are forwarded upstream unchanged. The routing fields `provider` and `models` are consumed by routing and removed before forwarding.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | Model slug in "vendor/model" form. |
| `messages` | object[] | Yes | The conversation turns, as the protocol defines. Forwarded unchanged. |
| `max_tokens` | integer | Yes | Upper bound on generated tokens. Required by the protocol; forwarded unchanged. |
| `stream` | boolean | No | When true, respond as a Server-Sent Events stream. Default: `false`. |
| `models` | string[] · max 3 | No | Models to fall back to, tried after `model` in array order when every deployment of the current model fails before its first byte (see Runtime fallback). Entries repeating `model` or an earlier entry are dropped; with no `model` the first entry is the primary. Every entry must exist (404 model_not_found naming it); more than 3 is validation_failed. Not forwarded upstream. |
| `provider` | object | No | Per-request provider routing preferences, applied when several providers serve the requested model. Limits (`only`, `ignore`, `data_collection`, `zdr`, `quantizations`) can only tighten the workspace routing settings; the sort mode (`order` and `sort`) is a default the request replaces as a whole. Provider names are platform ids, matched case-insensitively, plus the aliases `google-vertex` (gcp), `google-ai-studio` (gemini) and `amazon-bedrock` (aws). A known but unimplemented field is refused with `provider.<field> is not supported yet`; any other unknown key with `provider: unrecognized key "<key>"`. |
| `provider.order` | string[] | No | Providers to try first, in this order; providers not listed come after them. |
| `provider.only` | string[] | No | Use only these providers (intersected with the workspace allowlist). |
| `provider.ignore` | string[] | No | Never use these providers (added to the workspace ignore list). Wins over `only`. |
| `provider.allow_fallbacks` | boolean | No | With `false` and an `order`, only the providers in `order` may serve. A workspace `false` cannot be relaxed. Default: `true`. |
| `provider.sort` | string \| object | No | Rank providers within each `order` tier by `input_text + output_text` unit price, lowest first. Without it, deployment weight decides. |
| `provider.sort.by` | string | Yes | One of: `price`. |
| `provider.data_collection` | string | No | `deny` excludes providers that may train on prompts or retain them beyond a limited window (undeclared counts as may). One of: `allow`, `deny`. Default: `"allow"`. |
| `provider.zdr` | boolean | No | Only zero-data-retention providers. Default: `false`. |
| `provider.quantizations` | string[] | No | Only deployments at one of these precisions. One of: `fp4`, `int4`, `int8`, `fp8`, `fp16`, `bf16`, `fp32`, `unknown`. |

## Response

`200` `application/json`, `text/event-stream`

The message object, in the shape the Anthropic Messages protocol defines; the body is the upstream's, passed through. Streaming requests get an SSE stream of that protocol's named events (`event:` plus `data:` lines).

## Errors

| Status | Code | Description |
| --- | --- | --- |
| `400` | `invalid_request` | The request is missing a required field (`invalid_request`): a body with neither `model` nor `models` answers `model is required`. |
| `401` | `invalid_api_key`, `api_key_not_found`, `api_key_revoked`, `api_key_expired` | API key is missing or malformed (`invalid_api_key`), unknown (`api_key_not_found`), revoked (`api_key_revoked`), or expired (`api_key_expired`). |
| `402` | `insufficient_balance` | Wallet balance cannot cover the requested call (`insufficient_balance`). |
| `404` | `model_not_found`, `no_eligible_provider` | Requested model slug is unknown or not enabled (`model_not_found`), or the request's `provider` preferences, merged with the workspace routing settings, leave no provider that serves it (`no_eligible_provider`, with `error.metadata` carrying the routing funnel). |
| `422` | `validation_failed` | The request failed validation (`validation_failed`): the model is not served on this endpoint (the message lists the endpoints it is served on), or a routing field (`provider`, `models`, `route`, `preset`) breaks its rules. The message names the cause. |
| `429` | `rate_limited` | Request rejected by rate limiting (`rate_limited`). |
| `500` | `internal_error` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. |
| `503` | `provider_unavailable` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. |

Error body

```json
{
  "error": {
    "message": "No such model. Model names are vendor-qualified (vendor/model), so a name copied from a provider's own docs needs its vendor prefix added. Call GET /v1/models for the slugs this gateway serves.",
    "type": "not_found_error",
    "code": "model_not_found"
  }
}
```

## Response headers

Each header is listed with the response statuses it is sent with.

| Header | Sent with | Description |
| --- | --- | --- |
| `X-Pi-Trace-Id` | `200`, `400`, `401`, `402`, `404`, `422`, `429`, `default` | Platform request id, minted by the gateway. Present on every response of the inference endpoints; quote it when reporting a problem. |
| `Access-Control-Allow-Origin` | `200`, `400`, `401`, `402`, `404`, `422`, `429`, `default` | Always `*`. Credentials are the API key header, never a cookie. |
| `Access-Control-Expose-Headers` | `200`, `400`, `401`, `402`, `404`, `422`, `429`, `default` | Lists every `X-Pi-*` response header plus `Retry-After` and `Content-Encoding`, so browser code can read them. |
| `X-Pi-Provider` | `200`, `default` | Provider that served this response, in the same vocabulary as `provider.order` and `GET /v1/models/{vendor}/{model}/endpoints`. Present only when the upstream provider answered (a completion or an upstream error); after a fallback it names the provider that took over. |
| `X-Pi-Model` | `200`, `default` | Model slug that served this response - the same value as the body's `model`. When a fallback model took over, it is that model. Present only when the upstream provider answered. |
| `X-Pi-Upstream-Model` | `200`, `default` | The full model id the upstream provider itself reported in its response, passed on verbatim (on a stream, from the first data event). Absent when the upstream response named no model; never filled from the platform's own records. |
| `X-Pi-Upstream-Request-Id` | `200`, `default` | The upstream provider's own request id, passed on verbatim, for looking the call up on the provider's side. Absent when the provider returned none. |

## Request samples

cURL

```bash
curl https://api.pirouter.ai/v1/messages \
  -H "x-api-key: $PIROUTER_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5.5",
    "max_tokens": 1024,
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

Python

```python
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.pirouter.ai",
    api_key=os.environ["PIROUTER_API_KEY"],
)

message = client.messages.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
)
print(message.content[0].text)
```

TypeScript

```ts
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic({
  baseURL: 'https://api.pirouter.ai',
  apiKey: process.env.PIROUTER_API_KEY,
})

const message = await client.messages.create({
  model: 'anthropic/claude-sonnet-5.5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello!' }],
})
console.log(message.content[0].text)
```

## Response sample

`200` `application/json`

```json
{
  "id": "msg_011Cfx5SSMym...",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Hello! How can I help you today?"
    }
  ],
  "model": "anthropic/claude-sonnet-5.5",
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 12,
    "output_tokens": 13
  }
}
```

`200` `text/event-stream`

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

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

event: ping
data: {"type":"ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi there"}}

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":{"input_tokens":12,"output_tokens":18}}

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