What you will be able to do
- Describe a Messages API call as an HTTPS POST that sends and receives JSON, and name the headers the SDK sets for you
- Choose between a blocking call, the async client, streaming and submit-and-poll for a given workload
- Persist a conversation as JSON in your own storage between requests
- Explain why a network call is the natural place for asynchronous programming, and avoid shared-state races between coroutines that call the API
Key concept
One request, several ways to wait — Each pattern sends the same JSON Messages request and gets back the same Message object. Blocking, async, streaming and submit-and-poll differ only in how your code waits for that result, so you pick one based on latency and workload shape, not on what the response contains.
1.The Messages API as a REST endpoint that speaks JSON
Calling Claude is an ordinary REST call, so the conventions your other services already follow carry over. The API is served from https://api.anthropic.com, and a conversational request is POST /v1/messages. Every request has a JSON body with content-type: application/json, an anthropic-version header such as 2023-06-01, and credentials in Authorization: Bearer <token>. The older x-api-key header still works as a fallback.
The official SDKs set the authentication, version and content-type headers for you. They also add typed requests and responses, built-in retries, streaming support and request timeouts. With a raw HTTP client, all of that is your code to write. The one header you still pass yourself is anthropic-workspace-id, and only when your key covers more than one workspace.
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "Hello!"
}
],
"model": "claude-opus-5-5",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 12,
"output_tokens": 6
}
}Because the API keeps no conversation state, your service has to store it. The client sends the whole history with every request, as covered in [Stateless Messages API (client holds the conversation)](lesson:2.3). In practice, keep the messages array as plain JSON, a list of {"role", "content"} objects, in your own database or session store keyed by conversation. Load it, append the user's new turn, send it, then append the assistant's content and save it again. Earlier turns don't have to come from Claude, so a stored history can include synthetic assistant messages you wrote yourself.
That stored history grows with every turn, and request size is capped. Messages and Token Counting requests can be at most 32 MB, and anything larger returns 413 request_too_large. A long-lived chat that saves every turn forever will eventually hit that limit, so decide how history is trimmed before it does.
A REST call spends almost all of its time waiting on the network, and asynchronous programming is how a program uses that wait for other work instead of freezing. With the async client, await client.messages.create(...) suspends only the current coroutine; the event loop runs other coroutines until the response arrives. The request and the returned Message are identical to a blocking call, so the change is in how the waiting is scheduled, not in the JSON.
Interleaving brings a hazard that blocking code doesn't have. Coroutines can interleave, so several in-flight API calls that append to one shared list or dict can corrupt it, the same shared-mutable-state race that threads cause. Have each coroutine return its own result and combine them afterwards, rather than mutating shared state from inside the call. The API also offers asynchronous work at the endpoint level: the Message Batches API accepts requests now and processes them later, so the HTTP response only acknowledges the submission.
Checkpoint 1 of 5· Check yourself
A team writes a thin Go wrapper over raw HTTP instead of using an official SDK. Which header can they leave to the SDK only if they switch to one, and must otherwise send themselves on every request?
anthropic-version is required on every request, and the SDKs send it along with auth and content-type. anthropic-workspace-id is only needed with multi-workspace keys, and request-id is a response header.
“If you are using the Client SDKs, the SDK sends the authentication, version, and content-type headers automatically”Source: platform.claude.com
Checkpoint 2 of 5· Exam question
An engineer is processing a nightly batch of 50,000 support tickets through Claude to generate categorization labels. Each ticket is independent, latency is not a concern until the next morning, and the team wants to minimize cost. Which approach best fits this asynchronous workload?
Correct answer: A — Submit the tickets through the Batch API, which processes large volumes of independent requests asynchronously at roughly half the cost of standard synchronous calls
- A. Correct. Batch processing is designed exactly for large volumes of independent, non-latency-sensitive requests and is billed at a discount versus standard calls.
- B. Incorrect. Streaming is meant for incremental delivery of a single response, not for fanning out thousands of unrelated ticket classifications in one conversation.
- C. Incorrect. Looping synchronous calls with no batching or concurrency control wastes cost and ignores the discount and throughput benefits the Batch API offers.
- D. Incorrect. Resuming a session replays conversation context for a single continued exchange; it does not deduplicate billing across unrelated tickets.
2.Blocking calls and the async client
The simplest pattern is a blocking call. client.messages.create(...) on the synchronous Anthropic client waits until Claude has finished, then returns the full Message. That suits scripts, CLI tools and worker processes that handle one request at a time, where nothing else needs the thread while the request is in flight.
A service built on an event loop needs something else, because a blocking call there holds up every other coroutine while one request waits on the network. The Python SDK's AsyncAnthropic client has the same messages.create method, and you await it inside an async function. The request body and the returned Message are unchanged. The only difference is that your code yields while it waits. The async client can also use an aiohttp HTTP backend instead of the default one when you need better async throughput.
async def main() -> None:
message = await client.messages.create(
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Hello, Claude",
}
],
model="claude-opus-5-5",
)
print(message.content)
asyncio.run(main())Checkpoint 3 of 5· Fill the gap
Which keyword completes this AsyncAnthropic call so the coroutine waits for the response without blocking the event loop?
async def main() -> None:
message = ? client.messages.create(
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Hello, Claude",
}
],
model="claude-opus-5-5",
)
print(message.content)
asyncio.run(main())On AsyncAnthropic, messages.create returns an awaitable, so the coroutine must await it to get the Message. asyncio.run starts the event loop once, at the top level.
Sources4
3.Streaming when output is long or a user is watching
Streaming sits between a blocking call and a background job. The response arrives in pieces over one open HTTP connection, as covered in [Streaming over server-sent events](lesson:5.2). It wins in two situations. In the first, a person is waiting, so printing text as it arrives makes the response feel fast. In the second, the response is long enough that one silent connection would hit an HTTP timeout. For that second case the SDK requires streaming, even when you don't want partial text.
client = anthropic.Anthropic()
with client.messages.stream(
max_tokens=128000,
messages=[{"role": "user", "content": "Write a detailed analysis..."}],
model="claude-opus-5-5",
) as stream:
message = stream.get_final_message()Streaming also changes how failures reach you. A problem that a blocking call reports as an HTTP status can arrive mid-stream as an error event. Overload is the common case: an overloaded_error event where a non-streaming call would get HTTP 529. Error handling written only around the initial HTTP response will miss it. The Python SDK offers both sync and async streams, so streaming works with either client from the previous section.
Checkpoint 4 of 5· Check yourself
A streaming integration retries on HTTP 529 but users still see half-finished answers during peak load. What is the most likely gap?
Once a stream has started, an overload can be delivered as an overloaded_error event rather than an HTTP status, so stream handling needs its own error path.
“you may receive an overloaded_error, which would normally correspond to an HTTP 529 in a non-streaming context”Source: platform.claude.com
Sources5
4.Submit-and-poll for work nobody is waiting on
Submit-and-poll decouples sending work from collecting results. You submit a job, get back an ID, and check its status later. The Message Batches API, which handles large volumes of requests asynchronously at lower cost, is the standard example. Its pricing and limits are covered in [Message Batches API and realtime vs batch](lesson:2.3). For the code you write, what matters is the shape: a batch starts with processing_status set to in_progress and moves to ended once every request has finished and results are ready.
message_batch = None
while True:
message_batch = client.messages.batches.retrieve(MESSAGE_BATCH_ID)
if message_batch.processing_status == "ended":
break
print(f"Batch {MESSAGE_BATCH_ID} is still processing...")
time.sleep(60)
print(message_batch)Polling has a cost: the job ID has to outlive the process that submitted it. Store the batch id along with whatever you need to match results back to your records. The poller can then run as a separate scheduled job, and a restart doesn't lose the work. Choose this pattern when throughput and cost matter more than latency, for example nightly classification or evaluation runs. Don't choose it when a user is waiting for the answer.
| Pattern | How your code waits | Wins when |
|---|---|---|
Blocking messages.create | Thread waits for the full Message | Scripts and one-request-at-a-time workers |
AsyncAnthropic + await | Coroutine yields while the request is in flight | Services already running on an event loop |
messages.stream | Consumes events over one open connection | A user watches output, or large max_tokens would time out |
| Message Batches (submit-and-poll) | Stores an ID, checks processing_status later | Large volumes where nobody waits on a single result |
Checkpoint 5 of 5· Match them up
Match each workload to the call pattern that fits it
Tap a term, then the definition that fits it.
Bulk offline work suits submit-and-poll, a watching user suits streaming, an event-loop service suits the async client, and a simple script suits a blocking call.
“Message Batches API: Process large volumes of Messages requests asynchronously with 50% cost reduction (POST /v1/messages/batches)”Source: platform.claude.com
Sources6
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Streaming is only for UIs. A backend job that wants the whole answer should always use a plain blocking call.Why is that wrong?
With large max_tokens values, the SDK requires streaming to avoid HTTP timeouts. Calling
get_final_message()on the stream still returns the complete Message.Covered in Streaming when output is long or a user is watching
2.The API remembers earlier turns, so each request only needs the newest user message.Why is that wrong?
The Messages API is stateless. Your service has to store the history and send all of it on every request.
Covered in The Messages API as a REST endpoint that speaks JSON
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://platform.claude.com/docs/en/api/overviewOfficial docs
“The Claude API is a RESTful API at https://api.anthropic.com that provides programmatic access to Claude models”
↩︎ The Messages API as a REST endpoint that speaks JSON“If you exceed these limits, you'll receive a 413 request_too_large error.”
↩︎ The Messages API as a REST endpoint that speaks JSON“Message Batches API: Process large volumes of Messages requests asynchronously with 50% cost reduction (POST /v1/messages/batches)”
↩︎ The Messages API as a REST endpoint that speaks JSON“If you are using the Client SDKs, the SDK sends the authentication, version, and content-type headers automatically”
↩︎ Checkpoint“Message Batches API: Process large volumes of Messages requests asynchronously with 50% cost reduction (POST /v1/messages/batches)”
↩︎ Checkpoint - 2.
“The Messages API is stateless, which means that you always send the full conversational history to the API.”
↩︎ The Messages API as a REST endpoint that speaks JSON“The Messages API is stateless, which means that you always send the full conversational history to the API.”
↩︎ Exam trap 2 - 3.https://platform.claude.com/cookbook/tool-use-memory-cookbookSecondary source
“Recognizes similar pattern in async code (coroutines can interleave too)”
↩︎ The Messages API as a REST endpoint that speaks JSON - 4.
“For improved async performance, you can use the aiohttp HTTP backend instead of the default httpx2”
↩︎ Blocking calls and the async client - 5.
“The Python SDK allows both sync and async streams.”
↩︎ Streaming when output is long or a user is watching“the SDK provides a way to use streaming internally while returning the complete Message object, identical to what client.messages.create() returns”
↩︎ Key concept“This is especially useful for requests with large max_tokens values, where the SDK requires streaming to avoid HTTP timeouts.”
↩︎ Exam trap 1“This is especially useful for requests with large max_tokens values, where the SDK requires streaming to avoid HTTP timeouts.”
↩︎ Prediction“you may receive an overloaded_error, which would normally correspond to an HTTP 529 in a non-streaming context”
↩︎ Checkpoint - 6.
“It starts as in_progress, then updates to ended once all the requests in the batch have finished processing, and results are ready.”
↩︎ Submit-and-poll for work nobody is waiting on“You can implement a polling loop that checks the batch status periodically until processing has ended”
↩︎ Submit-and-poll for work nobody is waiting on