What You Need to Know
A Claude application communicates with the API over HTTP, with JSON as the wire format. The wire format is the format of the data that the application and the API exchange. A request is a JSON body that the application sends to an endpoint with a few headers. A response is a JSON document that the API returns. An official SDK is a typed client library that wraps that wire format. The SDK builds the request, sends it and converts the reply into objects. A transport is the channel that carries model output to a person: a single response, a stream of server-sent events or a websocket. This lesson covers that layer: what it does, and how to defend your choices when someone questions them.
This lesson uses one made-up example throughout: a software company that sells a CRM product, which stores a business's contacts and messages. The team adds an assistant to the product's inbox. A customer writes "Can we move our demo to Thursday?" to a sales person who uses the product. Claude drafts a reply while the sales person watches the reply as it forms. The company's server holds the API key and makes the call.
One Call, Two Views
To see what the SDK does, send the same request twice: once raw, once through the SDK. The raw form shows three headers. As of September 2026, the request sends the credential in Authorization: Bearer <key>. The API still accepts the older x-api-key header as a legacy fallback. You need anthropic-version and content-type either way. A fourth header, anthropic-workspace-id, is required when the key covers more than one workspace. The example uses httpx2, the HTTP library that the Python SDK itself now uses for its requests. So the example runs wherever the SDK is installed.
import os
import httpx2
customer_message = "Can we move our demo to Thursday?"
response = httpx2.post(
"https://api.anthropic.com/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['ANTHROPIC_API_KEY']}",
"anthropic-version": "2026-10-21",
"content-type": "application/json",
},
json={
"model": "claude-sonnet-5-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": customer_message}],
},
timeout=30.0,
)
print(response.status_code, response.headers["request-id"])
print("".join(b["text"] for b in response.json()["content"] if b["type"] == "text"))And here is the identical call through the SDK.
import anthropic
client = anthropic.Anthropic() # resolves ANTHROPIC_API_KEY from the environment
customer_message = "Can we move our demo to Thursday?"
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": customer_message}],
)
print(message._request_id)
print("".join(b.text for b in message.content if b.type == "text"))Both calls share the endpoint, the body, the billing and the rate limits. The response is ordinary JSON either way. It holds an id, a role, an array of typed content blocks, a stop_reason and a usage object. The API returns failures in a fixed envelope, not as free text. The envelope is one JSON object with a fixed structure. Its top-level type is error. A nested error object always carries a type and a message. The envelope also carries a request id, and the request-id response header carries the same id.
Everything the SDK does transforms that request and response. A typed request object serialises into the request body. A typed response object deserialises from the payload. A typed exception is the error envelope plus its status code. A stream is the same response, delivered as a sequence of events instead of one document. The practical test of this layer is to diagnose a failure. Given a failing call, can you tell from the status code alone whether the call will fail the same way next time? Can you produce the request id for support? At the company, the answers decide whether a failed draft is retried or fixed.
What the SDK Wraps
Anthropic provides client SDKs for seven languages: Python, TypeScript, C#, Go, Java, PHP and Ruby. It also provides the ant CLI. Anthropic states the scope of the SDKs in one sentence. The sentence is: "Each SDK provides idiomatic interfaces, type safety, and built-in support for streaming, retries, error handling, and OpenTelemetry tracing". That means:
- Credential resolution, and the
anthropic-versionheader, which the SDK sends for you. Each SDK major version sends one API version. As of October 2026 the current major versions send2026-10-21(from Python 2.0.0 and TypeScript 1.0.0) - Typed request construction and typed responses
- A typed exception hierarchy mapped from status codes
- Automatic retry, with backoff (a wait between attempts), of the transient class of errors, and a default timeout
- Server-sent events parsed into typed events, plus an accumulation helper, which collects those events
- One interface over synchronous and asynchronous clients
- The request id exposed as
_request_id"so that you can quickly log failing requests and report them back to Anthropic" - Auto-pagination over list endpoints
- Dedicated client classes for the cloud platforms
Whoever stops using the SDK becomes responsible for every item on that list. The company is no exception.
Each status code maps to a named exception. Learn the exact names.
| Status | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | UnprocessableEntityError |
| 429 | RateLimitError |
| >=500 | InternalServerError |
| transport failure | APIConnectionError |
Catch the most specific exception first. Branch on the exception class, never on the message text. The message text is not a contract, and it changes without notice. The handler below queues the request on an ordinary rate limit. On a connection failure, the handler uses a manual draft instead. In the product, the sales person writes that draft.
try:
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": customer_message}],
)
except anthropic.RateLimitError as err:
body = err.body if isinstance(err.body, dict) else {}
details = body.get("error", {}).get("details") or {}
if details.get("error_code") == "enforced_spend_limit_reached":
raise # tier spend cap: no retry clears it
queue_for_later(customer_message) # an ordinary rate limit, worth retrying later
except anthropic.BadRequestError as err:
log.error("malformed request %s", err.request_id) # will fail identically
except anthropic.APIConnectionError:
degrade_to_manual_draft()Not every 429 is transient. As of September 2026, the monthly spend cap of a usage tier also returns a 429. That 429 has no retry-after header, and its error.details.error_code is enforced_spend_limit_reached. The request "keeps failing until access resumes", and the SDK's own retries do not change that. The field check in the RateLimitError branch exists for this case. The check raises the error instead of queueing a request that cannot succeed.
People misread the retry and timeout defaults more than any other numbers. "Certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, because of a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default". And: "By default requests time out after 10 minutes". The two defaults multiply, because "requests that time out are retried twice by default". So the worst-case wall-clock time is roughly the timeout multiplied by the attempt count. Set both on purpose, at one layer.
client = anthropic.Anthropic(max_retries=2, timeout=30.0)
# or per request, without rebuilding the client
client.with_options(max_retries=0, timeout=5.0).messages.create(...)Your own three-attempt loop, in addition to the defaults, adds no resilience. That loop multiplies the attempts, and it re-sends deterministic failures that will never succeed. Set max_retries at one layer. Let the other layer decide the policy: degrade, queue or fail with a visible error. At the company, the SDK retries. The company's code decides whether a draft waits in a queue or returns to the sales person.
Pagination, the REST Detail the SDK Hides
List endpoints return results in pages, and a cursor marks your position. The contract is the same for every list endpoint. A request takes limit, plus an optional after_id or before_id. The response carries data, has_more, first_id and last_id. The docs say last_id "Can be used as the after_id for the next page". Page size defaults to 20, and the maximum is 1,000 (checked 30 September 2026). So one call against a busy account returns a partial list. The response has a 200 status, and only has_more: true shows that the list is partial.
A message batch is a group of requests that the API processes asynchronously. Suppose that the company's server sends weekly account summaries as message batches and lists its batches each evening. A client that reads data once and never checks has_more raises no error. That client stays correct until the account grows past one page. Then the client becomes wrong without warning. Here is the loop that you would otherwise be responsible for, beside the SDK line that replaces it.
import os
import httpx2
import anthropic
headers = {
"Authorization": f"Bearer {os.environ['ANTHROPIC_API_KEY']}",
"anthropic-version": "2026-10-21",
}
# By hand: follow the cursor until has_more is false
batch_ids, params = [], {"limit": 20}
while True:
response = httpx2.get(
"https://api.anthropic.com/v1/messages/batches",
headers=headers,
params=params,
timeout=30.0,
)
response.raise_for_status()
page = response.json()
batch_ids += [batch["id"] for batch in page["data"]]
if not page["has_more"]:
break
params["after_id"] = page["last_id"]
# Through the SDK: iterating the page fetches later pages as needed
client = anthropic.Anthropic()
sdk_ids = [batch.id for batch in client.messages.batches.list(limit=20)]
print(len(batch_ids), len(sdk_ids))The SDK docs state that "List methods in the Claude API are paginated". A plain for loop over the result reads every page. For an export that has to resume after an interruption, the page object also exposes has_next_page(), next_page_info() and get_next_page().
What the SDK Does Not Change
The SDK is a client library over a public HTTP endpoint. The SDK does not give access to different models. The model strings are the same strings that a raw request sends. The SDK does not change pricing. Anthropic bills tokens at the published per-model rates, whether the request came from the Python SDK, the CLI or an HTTP client. The SDK does not change rate limits. Your organisation's limits apply to the endpoint, not to the client. A 429 from the SDK and a 429 from a raw request mean the same thing. The SDK carries no service-level guarantee, no support entitlement and no contractual term. Those come from your agreement with Anthropic, not from a package manager.
The SDK also does not make an application reliable by itself. Its retry defaults help with the transient class of errors. They do nothing for a malformed request, an expired key or an oversized payload. Each of those fails the same way on every attempt. Nor does the SDK manage your context window, your prompt or your cost. Those are design decisions, and the rest of this domain covers them.
Reaching the Wire Without Dropping the SDK
The honest reasons to stop using the SDK are about access. That means access to the headers, the traffic, a field the types do not know yet, or an endpoint the SDK has not wrapped. For each of these, the SDK has a documented way to get that access. It is almost never right to stop using the SDK for HTTP access.
- Headers and status. Put
.with_raw_responsein front of a method to get the raw response, headers included (lesson 2.1 reads theanthropic-ratelimit-*family this way). Then.parse()gives you the object that the plain call would have returned..with_streaming_responsedoes the same without reading the body immediately. It works inside a context manager "so that the response will reliably be closed". TypeScript names the pair.withResponse()and.asResponse()respectively. - The traffic itself. Set
ANTHROPIC_LOG=debug, and the SDK logs its own traffic. Keep debug logging out of production. The TypeScript SDK docs warn that at debug level "all HTTP requests and responses are logged, including headers and bodies". So the SDK writes customer text to your log store. At the company, that text is customers' messages. Python has a trap here too (checked 30 September 2026). Tracing tools that patchhttpx, such as OpenTelemetry's instrumentor, miss the SDK'shttpx2requests. The fix is to callhttpx2.alias_httpx()"once at startup, before anything importshttpx". - Fields the types do not declare yet.
extra_body,extra_queryandextra_headerssend what the typed signature does not declare. They "override documented parameters of the same name", so give them trusted input only. Undeclared response fields remain available throughmodel_extra. - Endpoints the SDK has not wrapped. Call them anyway with
client.get,client.postand the other verbs. "Options on the client, such as retries, are respected". - Beta features.
client.betawith abetaslist is not limited to the SDK. It writes theanthropic-betaheader, which you add by hand to a raw request. The caveats of a beta include "different rate limits or pricing", and they apply equally to curl and to the SDK.
Transports, and the Layer the Websocket Belongs To
Three transport shapes cover what a Claude application does. The company can use all three.
A plain request/response call fits when the caller needs a complete answer before it can act. Batch jobs, classifiers and service-to-service calls fit here: anything whose consumer is code, not a person watching. The company's evening job that lists its batches is this shape.
Server-sent events fit when a person is waiting and should see the answer as it forms. The sales person watching a draft appear is this shape. The docs say: "When creating a Message, you can set "stream": true to incrementally stream the response using server-sent events (SSE)". SSE runs over ordinary HTTP and needs no protocol upgrade. It is one-directional by design. MDN says: "This is a one-way connection, so you can't send events from a client to a server". That suits a response, because the client has nothing to send while the model generates.
A websocket fits when your own application needs a persistent two-way channel. MDN says it makes it "possible to open a two-way interactive communication session between the user's browser and a server".
The two hops (browser to your server, and your server to Anthropic) sit at different layers, and Anthropic separates them in its own hosting guidance. An application container "exposes an HTTP or WebSocket endpoint" to its own clients. Separately, "The SDK needs outbound HTTPS to api.anthropic.com". Here are both hops in one file.
import Anthropic from "@anthropic-ai/sdk";
import { WebSocketServer } from "ws";
const client = new Anthropic();
const wss = new WebSocketServer({ port: 8080 }); // hop 1: browser <-> your server
wss.on("connection", (socket) => {
socket.on("message", async (raw) => {
// hop 2: your server <-> Anthropic, over HTTPS, streamed as SSE
const stream = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: raw.toString() }],
stream: true
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
socket.send(event.delta.text);
}
}
socket.send(JSON.stringify({ done: true }));
});
});A chat front end, such as the product's inbox screen, may keep a websocket open to its backend for typing indicators, cancellation, presence and multi-user state. The company's server then calls Claude over HTTPS and reads the SSE stream. No documented part of the Anthropic API accepts a websocket connection from a client. So opening a websocket to Anthropic to make text appear is wrong at the protocol level, not merely a weaker design. The reverse is also true. Streaming from the API does not need a websocket in your front end. A websocket in your front end does not give you streaming from the API. They are independent decisions at different layers.
What the Browser Hop Is For
Two properties justify the hop between the browser and your server. Neither is about speed.
The first is the credential. Using the TypeScript SDK in a browser requires an option called dangerouslyAllowBrowser, because enabling it "exposes your secret API credentials in the client-side code". The docs name two narrow exceptions: internal tools with trusted users, and development on short-lived credentials. A public front end that calls Anthropic directly is wrong on security grounds before the protocol question arises. The key is on your server, as it is at the company.
The second is the return channel, and it is the real argument for a websocket on that hop. SSE carries nothing from the browser. Yet a person who watches a draft as it forms often has something to send during generation. The sales person may press stop, or a newer message from the customer may arrive. The server converts that browser message into an action on the upstream request, the request to Anthropic. With the SDK, the action is one line: "If you need to cancel a stream, you can break from the loop or call stream.controller.abort()". Here is the earlier server, extended to honour a cancel.
import Anthropic from "@anthropic-ai/sdk";
import { WebSocketServer } from "ws";
const client = new Anthropic();
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
let active: AbortController | null = null;
let latest = 0; // one per message received; the browser counts the messages it sends
const cancel = () => {
latest += 1;
active?.abort();
};
socket.on("close", cancel); // a closed tab is an implicit cancel
socket.on("message", async (raw) => {
cancel(); // a cancel and a newer message both stop the current stream
const id = latest; // stamped on every frame this message produces
const send = (frame: object) => socket.send(JSON.stringify({ id, ...frame }));
try {
const msg = JSON.parse(raw.toString());
if (msg.type === "cancel") return;
const stream = await client.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: msg.text }],
stream: true
});
if (id !== latest) return stream.controller.abort(); // cancelled while connecting
active = stream.controller;
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
send({ type: "delta", text: event.delta.text });
}
}
send({ type: "done" });
} catch {
send({ type: "failed" }); // a bad message, a failed call, or an error frame after the 200
}
});
});A closed tab counts as a cancel. When a user leaves, as the sales person might, the server stops reading a stream that nobody will see. The counter handles a stop that the user presses while the request is still connecting. The server aborts that stream the moment it opens. The counter also labels every frame. The last deltas of an aborted stream, or its done, can still be in transit after a newer message starts streaming. The browser keeps the same count, one per message it sends. The browser ignores any frame whose id does not match. So a stop halts the text at once, and an old done can never close a newer answer. This per-connection state is the cost of a two-way channel. A request/response design never has to manage this state.
Streaming Mechanics, and the Failure After the 200
Setting stream: true changes one JSON document into a sequence of events. First comes message_start. Each content block then gets a content_block_start, one or more content_block_delta events and a content_block_stop. Next comes message_delta, which carries the final stop_reason and cumulative usage. Last comes message_stop. The SDKs parse those events. They offer an event iterator and an accumulating helper that returns the final Message.
Two mechanics matter beyond what the user sees. First, streaming is sometimes required. The trigger is expected duration, not any published max_tokens figure. The Python SDK "will throw a ValueError if a non-streaming request is expected to take longer than approximately 10 minutes". This is a client-side guard, and passing stream=True or overriding timeout disables it. The guard exists because some networks drop idle connections. A non-streaming request that runs longer than its timeout is then terminated and retried without a response.
Second, a stream can fail after it has opened with a 200. An HTTP 200 opens the stream. The docs warn: "The API might occasionally send errors in the event stream. For example, during periods of high usage, you might receive an overloaded_error". The error arrives as an event: error frame in the middle of the stream. Error handling that wraps only the call, and not the iteration, misses that error. The result is a truncated answer with no exception, such as a half-written draft on the product's inbox screen.
try:
with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": customer_message}],
) as stream:
for text in stream.text_stream: # the failure can land HERE
emit_to_sales_person(text)
final = stream.get_final_message()
except anthropic.APIStatusError as err:
mark_draft_incomplete(err.request_id)A third detail affects anyone who reconciles usage: "The token counts shown in the usage field of the message_delta event are cumulative". Read them. Do not sum them.
Client Lifecycle and Configuration
Construct one client and reuse it. The client holds an HTTP connection pool, a set of open connections that it reuses. Anthropic tells you to use the SDK's default HTTP client classes "to ensure the SDK's default configuration (such as timeouts and connection limits) is preserved". A client constructed inside every request handler discards that pool on every call. Each request pays the cost of a fresh TLS handshake (the exchange that sets up an encrypted connection), and open sockets multiply under concurrency. Under load, this defect shows as latency that grows with traffic and as connection errors that look like an upstream problem. The company's server should build its client once, at startup.
Configure through the environment, not through values written in the code. ANTHROPIC_API_KEY holds the credential. Set base_url or ANTHROPIC_BASE_URL when you route requests through a proxy or gateway. To target a cloud platform, construct that platform's dedicated client class. Do not configure the first-party client with a different base URL. Authentication, model-id shapes and endpoint routing all differ.
The async client is a different class, not a flag. AsyncAnthropic has the same methods as Anthropic. The docs state that the async client "uses the exact same interface" for streaming and for file uploads. So to move a synchronous integration, such as the company's, to async, you change the client and add an await. The call sites need no rewrite. For concurrent fan-out (many requests at the same time), the SDK provides an alternative HTTP backend, DefaultAioHttpClient. You install it with the anthropic[aiohttp] extra and pass it as http_client. The docs offer it "for improved async performance". Choosing async is a concurrency decision. It is a property of the client you construct, not of the code you write around it.
import asyncio
from anthropic import AsyncAnthropic, DefaultAioHttpClient
async def main() -> None:
async with AsyncAnthropic(http_client=DefaultAioHttpClient()) as client:
message = await client.messages.create(
model="claude-sonnet-5-5",
max_tokens=256,
messages=[{"role": "user", "content": "One line, please."}],
)
print("".join(b.text for b in message.content if b.type == "text"))
asyncio.run(main())You can pass your own http_client for a proxy or a bound local address. The docs say to "use DefaultHttpxClient and DefaultAsyncHttpxClient instead of raw httpx2.Client and httpx2.AsyncClient to ensure the SDK's default configuration (such as timeouts and connection limits) is preserved". A raw httpx2.Client silently discards the pool sizing and the timeout that the SDK would have set. That is the same defect as a client per request, from a different cause. The 2 matters. As of September 2026, the Python SDK "sends requests with httpx2, an API-compatible fork of httpx". And "Passing a client from the separate httpx package raises a TypeError". So older code that builds its own httpx.Client fails as soon as it is passed as http_client. The SDK also sets a TCP keep-alive to reduce idle-connection drops on networks that cut idle connections. A custom http_client overrides that keep-alive too.
The library closes connections when the client is garbage collected. It also exposes .close() and a context manager to close connections at a known time. Use those in a worker process, and do not rely on collection timing.
Treat JSON between your own services as a versioned interface. Validate it when your service receives it. Tolerate unknown fields. Do not assume a field exists because it existed in the last release. Structured-output patterns and defensive parsing of model output belong to Domain 6, and secrets handling belongs to Domain 7. This is the ordinary service-to-service discipline that both domains depend on.
Versions: Three Numbers, Three Contracts
Three version identifiers apply to every call. Each one promises something different.
The anthropic-version header pins the API contract. Within a version, Anthropic preserves existing inputs and outputs. It may still "Add additional values to the output" and "Change conditions for specific error types". It may also "Add new variants to enum-like output values (for example, streaming event types)". Write your parser so that it accepts those changes. A strict schema that rejects unknown response fields fails because of its own strictness. A match that raises on an unfamiliar stop reason, block type or event type fails for the same reason. That code is a defect that a future release will expose. At the company, such code would then fail on a response that carries a new value. The streaming docs say plainly that "your code should handle unknown event types gracefully". Select content blocks by type, never by position. On models that think adaptively, a thinking block can come before the text.
Breaking changes arrive only with a new version, and you choose when to move to it. Under 2026-10-21, temperature, top_p and top_k are removed. A request that sends any of them gets a 400 on every model. A request on 2023-06-01 keeps the old contract.
def reply_text(message) -> str:
# Thinking blocks, tool calls and block types added after this was
# written are skipped rather than fatal.
return "".join(block.text for block in message.content if block.type == "text")Beta names are the second contract, and a weaker one. A beta may "Have breaking changes with notice" or "Be deprecated or removed". An invalid beta name returns a 400. A beta name that your organisation has no access to also returns a 400. Keep each beta behind a switch that you can disable, and record its name in one place.
The SDK package version is the third. The Python SDK "generally follows SemVer conventions". SemVer is semantic versioning, where only a major version carries breaking changes. But "certain backward-incompatible changes may be released as minor versions", including changes that only affect static types. Pin it in the lockfile and upgrade on purpose. When a documented feature seems to be missing, check anthropic.__version__ before blaming the docs.
In the exam
The guide describes this sub-skill in one line: "Foundational technical concepts supporting AI application development, including basic engineering practices (integrating with SDKs that wrap REST APIs, websockets)". It is the largest sub-skill in the domain.
This topic has two halves: what the SDK wraps and what it does not change. Questions are written on the second half, because every plausible-sounding wrong answer comes from there. The exam-safe statement is that the SDK changes how convenient it is to call the API, and nothing about the API itself. An option that offers an SDK-only model, an SDK discount, higher or waived rate limits, guaranteed uptime or immunity from an error class is invented. Such an option is wrong by definition.
Websockets are in scope. The question pattern that appears reliably is a developer who proposes the right transport at the wrong layer, such as a websocket opened to Anthropic. Claims that API streaming needs a front-end websocket, or that a front-end websocket gives you API streaming, are equally testable.
Some questions describe latency that grows with traffic, plus connection errors that look like an upstream problem. Such a question usually describes a client constructed per request. The async client and the http_client override are the kind of mechanism this exam tests. Both are easy to get subtly wrong.
Know the boundaries. Engineering discipline around the code (version control, review, refactoring safety and writing async correctly) belongs to sub-skill 2.4. The full event catalogue and delta-assembly rules belong to sub-skill 2.3. This sub-skill covers which transport carries the events and what can go wrong in transit. Choosing the async client belongs here, not in Domain 2, because it is a property of the client you construct.
Exam Traps
Crediting the SDK with a benefit such as cheaper tokens, exclusive models or higher rate limits
The SDK is a typed wrapper over the same endpoint, so models, prices and rate limits are identical either way. It changes how convenient it is to call the API and nothing about the API. Credit the API or your agreement instead.
Opening a websocket to Anthropic to stream model output, or claiming a browser needs one to receive streamed text
The API streams over server-sent events on ordinary HTTP. A websocket, if the app needs one, belongs between the browser and your own backend, which consumes the API's event stream over HTTPS.
Reading the SDK retry defaults as general resilience, or setting a long timeout so slow requests always succeed
The SDK retries connection errors, 408, 409, 429 and 5xx by default, including the spend-cap 429 that no retry clears. Timeouts multiply with retries: worst-case exposure is the timeout multiplied by the attempts. Set both on purpose, at one layer.
Treating a 200 as the end of the risk on a streamed request
A 200 only means the stream opened. An error can arrive as an event inside it, so handling that wraps the call but not the iteration misses it and leaves a truncated answer with no exception. Handle error events on the iteration.
Practice Scenario
An online retailer is adding a Claude-backed assistant that drafts a reply to an order question while the support employee is still typing. Three design arguments are unresolved. One developer wants to drop the official SDK and post to the endpoint with a plain HTTP client, to avoid a dependency. A second insists the support browser must hold a websocket open to Anthropic so the employee sees the draft appear word by word. A third has written the integration so a new client object is constructed inside every request handler, with no timeout set. Which assessment is correct?
Build Exercise
Read the Wire, Then Take the SDK Away and Give It Back
What you'll learn
- How to read a raw Claude request and response, headers and error envelope included
- Which failures the SDK retries for you and which it raises immediately
- How timeouts and retries combine into the worst-case wall-clock time
- Which transport sits at which layer between a browser, your server and Anthropic
- Why a reused client behaves differently from a per-request one under concurrency
- Send one request twice: once with a raw HTTP client showing the endpoint, the three required headers and the JSON body explicitly, and once through the official SDK for the same language. Print both responses, check that they are the same document, then list every line of code the SDK version did not need
Why: You only understand what the SDK does when you have watched the same bytes come back through both paths.
You should see: Two structurally identical JSON responses, and a list of roughly a dozen concerns the SDK handled silently.
- Instrument the SDK call to expose the request id and status on success as well as on failure, and log both beside your own correlation id
Why: The request id is the only key shared between your logs and Anthropic support, and finding it during an incident is too late.
You should see: A request id on every logged call, matching the request-id response header.
- Force each error class deliberately and record what the SDK raised: a malformed body, a bad credential, an oversized payload and a rate-limit burst. Note which the SDK retried on your behalf and which it raised immediately, then reconcile that against the documented retried set
Why: The retryable and non-retryable boundary is the same boundary you use to choose how to recover, and guessing it causes retry storms, where failed requests are retried again and again.
You should see: BadRequestError and AuthenticationError raised immediately; RateLimitError and 5xx retried before raising.
- Configure the client three ways and measure worst-case wall clock: default timeout with default retries, a short timeout with retries left alone, and retries disabled. Write down how timeout and attempt count compose
Why: A badly chosen timeout turns one slow request into several, and the arithmetic is not obvious until you have watched it.
You should see: Roughly the timeout multiplied by the attempt count in the first two configurations.
- Turn on streaming and consume the events directly rather than through the accumulation helper, printing each event type as it arrives. Then re-run with the helper and confirm you get the same final message, and note when you would choose each
Why: Knowing what the helper hides is what lets you decide whether you need it.
You should see: message_start, content_block_start, deltas, content_block_stop, message_delta, message_stop, and the same final text either way.
- Prove the layer separation on your own machine: run a small server that exposes a WebSocket endpoint to a browser page, have that server call Claude with stream true, and forward each text delta to the browser over the websocket. Draw the two hops and label which one is SSE and which one is the websocket
Why: Once you have built both hops yourself, the question that puts a websocket at the wrong layer stops being tempting.
You should see: Text appearing word by word in the browser, with no websocket connection to api.anthropic.com.
- Replace your per-request client with one constructed at startup and reused, run the same concurrent load against both versions, and record the difference in latency and in open sockets
Why: This defect only shows under concurrency, which is exactly the condition a question will describe.
You should see: Higher and rising latency plus many more open sockets on the per-request version.
Sources
- Messages — Claude Docs, the wire shape, required headers and the usage fields
- API overview — Claude Docs, the request headers, with Authorization: Bearer as the credential header and x-api-key as the legacy fallback
- CLI, SDKs, and libraries — Claude Docs, the seven client SDK languages and what each SDK provides
- Python SDK — Claude Docs, typed errors, retry and timeout defaults, request ids, client configuration, auto-pagination, raw and streaming response access, logging, undocumented requests, extra response fields and the SDK's versioning policy
- TypeScript SDK — Claude Docs, cancelling a stream, the dangerouslyAllowBrowser warning, the debug-level logging warning and the raw response methods
- Versions — Claude Docs, what the anthropic-version header preserves and what Anthropic may add within a version
- Migrating to API version 2026-10-21 — Claude Docs, the breaking changes in 2026-10-21, including the removed sampling parameters, and the first SDK major version that sends it
- Beta headers — Claude Docs, the anthropic-beta header, the SDK betas parameter, beta stability caveats and the 400 on an invalid beta name
- List Message Batches — Claude Docs, the cursor pagination contract (limit, after_id, before_id, has_more, first_id, last_id)
- Streaming messages — Claude Docs, SSE, the event sequence, cumulative usage and mid-stream error frames
- Claude API errors — Claude Docs, the error envelope and the request id
- Rate limits — Claude Docs, the spend-cap 429 with no retry-after header and its enforced_spend_limit_reached error code
- Hosting the Agent SDK — Claude Docs, the inbound HTTP or WebSocket port and the outbound HTTPS requirement
- Using server-sent events — MDN Web Docs, SSE as a one-way connection over HTTP
- The WebSocket API (WebSockets) — MDN Web Docs, two-way interactive sessions between a browser and a server
- Claude Certified Developer Foundations Exam Guide — Domain 5, Technical Fundamentals — Anthropic