Messages (Anthropic format)
POST /v1/messages creates a model response in the Anthropic Messages format, which Claude Code and the Anthropic SDKs send. The base URL is the origin only, https://api.zdr.sh: the SDKs append /v1/messages themselves.
Headers#
| Header | Notes |
|---|---|
x-api-key | The duck- key. Authorization: Bearer is accepted instead |
anthropic-version | Any value is accepted; the answer uses the current format |
anthropic-beta | Accepted as an open list; a beta the model does not know is ignored |
POST /v1/messages?beta=true is accepted.
Request#
| Field | Type | Notes |
|---|---|---|
model | string | Required. A model id from GET /v1/models. A claude- id is mapped when the key's Claude mapping is on, which is the default; otherwise it is refused with not_found_model |
max_tokens | integer | Required. The output cap, reasoning included |
messages | array | user and assistant turns. Content is a string or blocks: text, image, tool_use, tool_result, thinking |
system | string or blocks | The system prompt, kept at the front of the prefix |
stream | boolean | Default false. With true, Anthropic server-sent events |
tools | array | { "name", "description", "input_schema" } |
tool_choice | object | {"type": "auto"}, {"type": "any"}, {"type": "tool", "name": "..."} or {"type": "none"} |
thinking | object | {"type": "enabled", "budget_tokens": N} or {"type": "adaptive"} on reasoning models |
output_config | object | effort is read as the reasoning effort |
metadata | object | user_id names the end user; other flat entries are kept for attribution and never sent to a model |
stop_sequences | array | Stop sequences |
Documents arrive as blocks with a source of base64 or url (PDFs). Server tools such as web_search_20250305 are accepted when the model supports them; otherwise they are left out and x-token-notice says <tool> unavailable.
Thinking blocks#
Reasoning comes back as thinking blocks, streamed as content_block_delta events with thinking_delta and signature_delta. The signature is issued by zdr.sh. Send the block back unchanged on the next turn when your flow replays it.
Tool use#
The model calls tools with tool_use blocks carrying an id, a name and a JSON input. Answer with a tool_result block that carries the same tool_use_id. Tool call ids are rewritten, so an id you received is valid to send back and one you invent is not.
Response#
| Field | Type | Notes |
|---|---|---|
id | string | msg_ followed by random characters |
model | string | The id you asked for, or the id the Claude mapping chose |
content | array | text, thinking and tool_use blocks |
stop_reason | string | end_turn, max_tokens, stop_sequence, tool_use or refusal |
usage.input_tokens | integer | Uncached input tokens |
usage.output_tokens | integer | Generated tokens, thinking included |
usage.cache_read_input_tokens, usage.cache_creation_input_tokens | integer | Cached input and cache writes. A cache write is free |
ai_generated, ai_notice | boolean, string | The AI marker. AI transparency |
Count tokens#
POST /v1/messages/count_tokens returns {"input_tokens": N} for a payload without running it. Nothing is billed. A count uses one request of the key's rate limit and one account concurrency slot, exactly as the request it sizes.
Errors and headers#
Errors keep the Anthropic shape with the request id: {"type": "error", "error": {"type": "...", "message": "..."}, "request_id": "..."}. A 503 is overloaded_error, a 401 authentication_error. Errors lists the classes and the retry rules.
The headers are the same as on the OpenAI endpoints: x-request-id, x-token-cost, x-ratelimit-*, x-token-cache, x-token-notice, x-ai-generated and x-ai-model, and retry-after on a 429 or 503.