One messages.create call, end to end
messages.create(...)POST /v1/messages, JSON bodyretry-afterMessage JSON_request_idWhat the REST layer actually requires
The Claude API is a plain REST API. Messages are created with POST /v1/messages at https://api.anthropic.com, the body is JSON, and the response is JSON. Three headers matter on every raw call: the credential, anthropic-version — a dated API version string such as 2023-06-01 — and content-type: application/json. Keys can be presented as a bearer token in Authorization, and x-api-key remains accepted; a key that spans several workspaces also needs anthropic-workspace-id.
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello, Claude!"}]
}'anthropic-version is the part people forget. It is a dated contract: pinning it means a later change to the API's shape does not silently alter what your code receives. The SDKs send it for you, which is one reason a hand-rolled client is a maintenance liability rather than a saving.
What the SDK adds on top
Official SDKs exist for Python, TypeScript, C#, Go, Java, PHP and Ruby. They are thin, but the things they are thin about are exactly the things that bite in production.
| Concern | Raw REST | Official SDK (Python) |
|---|---|---|
| Auth and versioning | You set every header | Sent automatically; key read from ANTHROPIC_API_KEY |
| Transient failures | You write the backoff | Retries 408, 409, 429, 5xx and connection errors — max_retries defaults to 2 |
| Timeouts | Whatever your HTTP client defaults to | 10 minutes by default; set per client or per request |
| Errors | Status codes and JSON bodies | Typed classes: RateLimitError, BadRequestError, APITimeoutError… |
| Streaming | You parse the SSE frames | messages.stream(...) with text_stream and get_final_message() |
| Concurrency | Your own async plumbing | AsyncAnthropic alongside the synchronous client |
| Support diagnostics | Read the response header | message._request_id on the returned object |
| Beta features | Hand-written beta headers | client.beta.messages.create(..., betas=[…]) |
from anthropic import Anthropic, RateLimitError, APITimeoutError
# One place for policy: retries and timeout live on the client.
client = Anthropic(max_retries=4, timeout=30.0) # key read from ANTHROPIC_API_KEY
try:
msg = client.with_options(timeout=120.0).messages.create(
model="claude-sonnet-5", max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
except RateLimitError:
# The SDK already backed off max_retries times before raising.
queue_for_later(prompt)
except APITimeoutError:
fall_back_to_cached_answer()
else:
log.info("ok", request_id=msg._request_id, tokens=msg.usage.output_tokens)Streaming: server-sent events, not websockets
A long answer can take a long time. With stream set to true the API returns the response incrementally as server-sent events over ordinary HTTP — a single request whose response body arrives in labelled frames. The user sees text appear immediately, and your connection is never idle long enough to look dead to a proxy.
The event sequence of one streamed response
- Message start
message_start— empty shell - Block start
content_block_startper block - Block deltas
content_block_delta— the pieces - Block stop
content_block_stop— block complete - Message end
message_deltahasstop_reason, usage
blocks repeat — ping frames may appear anywhere
message_delta carries the stop_reason and cumulative usage — so a streamed call is only finished when you have read it, not when text stops arriving.Deltas come in kinds. Text arrives as text_delta; a tool call's arguments arrive as input_json_delta — partial JSON strings you must accumulate until content_block_stop before parsing, because no intermediate fragment is valid JSON; reasoning arrives as thinking_delta followed by a signature_delta. Errors can arrive mid-stream as an error event, for example an overloaded_error, which is why a streamed call needs the same failure handling as any other.
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
// Show tokens as they arrive.
stream.on("text", (chunk) => process.stdout.write(chunk));
// The helper accumulates the frames into one complete Message.
const message = await stream.finalMessage();
if (message.stop_reason === "max_tokens") {
markTruncated(message); // text stopped arriving — that is not "finished"
}Which shape of call does this workload need?
- A person is waitingStream itSSE; first token in under a second
- Machine-to-machinePlain requestsimpler to parse and test
- Very long outputStream itavoids long idle connections
- Bulk, not urgentBatch itcheaper; covered in Domain 1
Ordinary engineering practice, applied here
The rest of this objective is the general practice the exam guide expects of one to five years of engineering, made specific by the fact that calls are slow and metered. Four habits carry most of it.
- Do not block on I/O. A Claude call takes seconds, not milliseconds. Use the asynchronous client and bounded concurrency so a handful of slow calls cannot exhaust a thread pool or a connection limit.
- Configure timeouts deliberately. The default is generous — ten minutes — because long generations are legitimate. An interactive path needs a much shorter one plus a fallback, set per request rather than globally.
- Keep configuration out of the code. Model id, effort, limits and the key itself come from environment or config, so a model change is a deploy setting rather than a code change. Key handling specifically is covered in 7.4.
- Log the request id and the usage.
_request_idis the identifier support can look up;usageis the only honest source for cost. Both are on the response object already.
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Wrapping SDK calls in your own retry loop | Set max_retries on the client; two layers multiply attempts and cost. |
| Hand-rolling an HTTP client to “avoid a dependency” | Wrap the official SDK in a thin adapter and inherit its failure handling. |
Omitting anthropic-version on raw REST calls | Send the dated version header on every request so the contract is pinned. |
Parsing input_json_delta fragments as they arrive | Accumulate partial JSON until content_block_stop, then parse once. |
| Treating the end of the text stream as success | Read stop_reason and usage from message_delta before using the result. |
Serial for loops over hundreds of documents | Use the async client with bounded concurrency, or the Batch API. |
You should now be able to
- Describe the REST call an SDK makes, including endpoint, body and required headers.
- Name what the official SDKs add — retries, timeouts, typed errors, streaming helpers, request ids — and configure each.
- Implement streaming and handle the event sequence, including partial tool-input JSON and mid-stream errors.
- Distinguish server-sent events from websockets and say which the Claude API uses.
- Apply async, concurrency limits, timeouts and externalised configuration to a Claude integration.