Skip to content
Domain 5Task 5.2

Technical Fundamentals

Learn this interactively

What You Need to Know

A Claude application talks to the API over HTTP, with JSON as the wire format. A request is a JSON body sent to an endpoint with a few headers, and a response is a JSON document sent back. An official SDK is a typed client library that wraps that wire. It builds the request, sends it and turns 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 under questioning.

Take a dental practice adding an assistant to its reception desk. A patient writes "Can I move my check-up to Thursday?", and Claude drafts a reply while the receptionist watches it form. The practice'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 code samples use a metals counter's stock enquiry; the mechanics are identical. The raw form shows three headers. As of September 2026, the credential goes in Authorization: Bearer <key>. The older x-api-key header is still accepted as a legacy fallback. You need anthropic-version and content-type either way. A fourth header, anthropic-workspace-id, is required when the key spans more than one workspace. The example uses httpx2, the HTTP library the Python SDK itself now uses for its requests. So it runs wherever the SDK is installed.

python
import os
import httpx2

stock_enquiry = "Do you have 2 mm pewter sheet in stock?"

response = httpx2.post(
    "https://api.anthropic.com/v1/messages",
    headers={
        "Authorization": f"Bearer {os.environ['ANTHROPIC_API_KEY']}",
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    },
    json={
        "model": "claude-sonnet-5-5",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": stock_enquiry}],
    },
    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.

python
import anthropic

client = anthropic.Anthropic()  # resolves ANTHROPIC_API_KEY from the environment

stock_enquiry = "Do you have 2 mm pewter sheet in stock?"

message = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": stock_enquiry}],
)

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. Failures arrive in a fixed envelope, not as free text. The 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 same id rides on the request-id response header.

Everything the SDK does is a transformation of 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 diagnostic. Given a failing call, can you tell from the status code alone whether it will fail the same way next time? Can you produce the request id for support? At the practice, that decides whether a failed draft is retried or fixed.

What the SDK Wraps

Anthropic ships client SDKs for seven languages: Python, TypeScript, C#, Go, Java, PHP and Ruby. It also ships the ant CLI. It describes the SDKs' remit in one sentence: "Each SDK provides idiomatic interfaces, type safety, and built-in support for streaming, retries, and error handling". That means:

  • Credential resolution, and the anthropic-version header sent for you, set to 2023-06-01
  • Typed request construction and typed responses
  • A typed exception hierarchy mapped from status codes
  • Automatic retry of the transient class, with backoff, and a default timeout
  • Server-sent events parsed into typed events, plus an accumulation helper
  • 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 drops the SDK owns every item on that list, the practice included.

Each status code maps to a named exception. Learn the exact names.

StatusException
400BadRequestError
401AuthenticationError
403PermissionDeniedError
404NotFoundError
409ConflictError
422UnprocessableEntityError
429RateLimitError
>=500InternalServerError
transport failureAPIConnectionError

Catch them most-specific-first. Branch on the exception class, never on the message text. The message text is not a contract and changes without notice. In the handler below, an ordinary rate limit queues the request, and a connection failure falls back to a manual draft. At the practice, the receptionist writes that draft.

python
try:
    message = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": stock_enquiry}],
    )
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(stock_enquiry)          # 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, a usage tier's monthly spend cap also returns a 429. It has no retry-after header, and its error.details.error_code is enforced_spend_limit_reached. It "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. It raises 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 compound, 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.

python
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 on top of the defaults adds no resilience. It multiplies attempts and 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 loudly. At the practice, the SDK retries. The practice's code decides whether a draft waits in a queue or goes back to the receptionist.

Pagination, the REST Detail the SDK Hides

List endpoints return results in pages, and a cursor marks your place. The contract is the same from one endpoint to the next. 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 tops out at 1,000 (checked 30 September 2026). So one call against a busy account returns a partial list, with a 200 and only has_more: true to show it.

Say the practice's server lists its message batches each evening. A client that reads data once and never checks has_more raises no error. It stays correct until the account grows past one page, then goes wrong without warning. Here is the loop you would otherwise own, beside the SDK line that replaces it.

python
import os
import httpx2
import anthropic

headers = {
    "Authorization": f"Bearer {os.environ['ANTHROPIC_API_KEY']}",
    "anthropic-version": "2023-06-01",
}

# 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 crosses every page. For an export that has to resume after a break, 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. It does not give access to different models. The model strings are the same strings a raw request sends. It does not change pricing. Tokens bill at the published per-model rates, whether the request came from the Python SDK, the CLI or an HTTP client. It does not change rate limits. Your organisation's limits apply to the endpoint, not to the client, and a 429 is a 429 either way. It 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. 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 drop the SDK are about reach: the headers, the traffic, a field the types do not know yet, or an endpoint the SDK has not wrapped. Each of these has a documented escape hatch. Dropping the SDK to reach the wire is almost never right.

  • Headers and status. Put .with_raw_response in front of a method to get the raw response, headers included (2.1 reads the anthropic-ratelimit-* family this way). Then .parse() gives you the object the plain call would have returned. .with_streaming_response does the same without reading the body up front. It works inside a context manager "so that the response will reliably be closed". TypeScript spells the pair .withResponse() and .asResponse() respectively.
  • The traffic itself. Set ANTHROPIC_LOG=debug and the SDK logs its own traffic. Keep that out of production. The TypeScript SDK docs warn that at debug level "all HTTP requests and responses are logged, including headers and bodies". So customer text lands in your log store, which at the practice means patients' messages. Python has a trap here too (checked 30 September 2026). Tracing tools that patch httpx, such as OpenTelemetry's instrumentor, miss the SDK's httpx2 requests. The fix is to call httpx2.alias_httpx() "once at startup, before anything imports httpx".
  • Fields the types lag behind. extra_body, extra_query and extra_headers send what the typed signature does not declare. They "override documented parameters of the same name", so feed them trusted input only. Undeclared response fields stay reachable through model_extra.
  • Endpoints the SDK has not wrapped. Call them anyway with client.get, client.post and the other verbs. "Options on the client, such as retries, are respected".
  • Beta features. client.beta with a betas list is no SDK privilege. It writes the anthropic-beta header that a raw request sends by hand. A beta's own caveats, "different rate limits or pricing" among them, 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 practice 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 practice's evening job that lists its batches is this shape.

Server-sent events fit when a person is waiting and should see the answer form. The receptionist 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 say 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 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.

typescript
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 reception screen, may hold a websocket to its backend for typing indicators, cancellation, presence and multi-user state. The practice's server then calls Claude over HTTPS and consumes SSE. No documented Anthropic API surface 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 holds too. 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. Browser use of the TypeScript SDK sits behind 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 lives on your server, as it does at the practice.

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 watching a draft form often has something to say mid-generation. The receptionist may press stop, or a newer message from the patient may arrive. The server turns that message into an action on the upstream request. The SDK makes the action 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 it.

typescript
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 enquiry 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 walks away, as the receptionist might, the server stops reading a stream nobody will see. The counter covers a stop pressed while the request is still connecting: that stream is aborted the moment it opens. The counter also stamps every frame. An aborted stream's last deltas, or its done, can still be in flight after a newer message has begun streaming. The browser keeps the same count, one per message it sends. It ignores any frame whose id does not match. So a stop freezes the text at once, and an old done can never close a newer answer. This per-connection state is the price of a two-way channel. A request/response design never has to deal with it.

Streaming Mechanics, and the Failure After the 200

Setting stream: true turns 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, carrying the final stop_reason and cumulative usage. Last comes message_stop. The SDKs parse that framing and offer an event iterator and an accumulating helper that hands back 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 switches it off. The guard exists because some networks drop idle connections. A non-streaming request that outlives its timeout is then terminated and retried without a response.

Second, a stream can fail after it has succeeded. An HTTP 200 opens the stream. The docs warn: "The API may occasionally send errors in the event stream. For example, during periods of high usage, you may receive an overloaded_error". It arrives as an event: error frame mid-stream. Error handling that wraps only the call, and not the iteration, misses it. The result is a truncated answer with no exception, such as a half-written draft on the reception screen.

python
try:
    with client.messages.stream(
        model="claude-sonnet-5-5",
        max_tokens=2048,
        messages=[{"role": "user", "content": stock_enquiry}],
    ) as stream:
        for text in stream.text_stream:      # the failure can land HERE
            emit_to_counter_clerk(text)
        final = stream.get_final_message()
except anthropic.APIStatusError as err:
    mark_draft_incomplete(err.request_id)

A third detail affects anyone reconciling usage: "The token counts shown in the usage field of the message_delta event are cumulative". Read them; do not add them up.

Client Lifecycle and Configuration

Construct one client and reuse it. The client owns an HTTP connection pool. 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 throws that pool away on every call. Each request pays for a fresh TLS handshake, and open sockets multiply under concurrency. Under load it shows as latency that grows with traffic and connection errors that look like an upstream problem. The practice's server should build its client once, at startup.

Configure through the environment, not through literals. ANTHROPIC_API_KEY holds the credential. Set base_url or ANTHROPIC_BASE_URL when routing through a proxy or gateway. To target a cloud platform, construct that platform's dedicated client class. Do not point the first-party client at a different base URL. Authentication, model-id shapes and endpoint routing all differ.

The async client is a different class, not a flag. AsyncAnthropic mirrors Anthropic method for method. The docs state the async client "uses the exact same interface" for streaming and for file uploads. So moving a synchronous integration to async, such as the practice's, means a client swap and an await. The call sites need no rewrite. For concurrent fan-out the SDK ships 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.

python
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 the SDK would have set. That is the same defect as a client per request, arriving by a different route. 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 the moment it is passed as http_client. The SDK also sets a TCP keep-alive to blunt idle-connection drops on networks that cut them. A custom http_client overrides that too.

The library closes connections when the client is garbage collected. It also exposes .close() and a context manager for deterministic teardown. 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 at the boundary. Tolerate unknown fields. Do not assume a field exists because it did last release. Structured-output patterns and defensive parsing of model output are Domain 6, and secrets handling is Domain 7. This is the ordinary service-to-service discipline underneath both.

Versions: Three Numbers, Three Contracts

Three version identifiers sit under 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", "Change conditions for specific error types" and "Add new variants to enum-like output values (for example, streaming event types)". Read those reservations as instructions to your parser. A strict schema that rejects unknown response fields breaks itself. So does a match that raises on an unfamiliar stop reason, block type or event type. That code is a defect waiting for a release. At the practice, a response carrying a new value would then fail. 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.

python
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, and so does one your organisation has no access to. Keep each beta behind a switch you can turn off, with its name recorded in one place.

The SDK package version is the third. The Python SDK "generally follows SemVer conventions". 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. Items are written on the second half, because every plausible-sounding wrong answer lives there. The exam-safe formulation is that the SDK changes the ergonomics of calling the API and nothing about the API itself. An option offering an SDK-only model, an SDK discount, higher or waived rate limits, guaranteed uptime or immunity from an error class is invented and wrong by construction.

Websockets are in scope. The reliable item shape is a developer proposing 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 one gives you API streaming, are equally testable.

A stem that describes latency growing with traffic, plus connection errors that look upstream, is usually describing 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 2.4. The full event catalogue and delta-assembly rules belong to 2.3. This sub-skill owns 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

Exam Trap

Accepting an invented SDK-only benefit, such as cheaper tokens, exclusive models, higher rate limits or an uptime guarantee.

The SDK is a typed client over the same public HTTP endpoint. Models, prices, rate limits and contractual terms are properties of the API and your agreement, not of a package. The SDK changes the ergonomics of calling the API and nothing about the API itself.

Exam Trap

Opening a websocket to Anthropic to stream model output, or claiming a browser cannot receive streamed text without one.

The Messages API streams over server-sent events with stream: true, which is one-directional and runs over ordinary HTTP. A websocket belongs between your own client and your own server, exactly as Anthropic hosting guidance describes exposing an HTTP or WebSocket port while the server calls the API over outbound HTTPS.

Exam Trap

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 twice by default with backoff, but the retried set is not the transient class: a 409 is fix-then-retry. The one 429 that is not transient is the usage tier spend cap: the SDK retries it anyway, and it keeps failing until access resumes. A 400, 401 or oversized payload is re-sent by nobody sensible and fixed by changing the request. Timeouts also compose with retries, so wall-clock exposure is the timeout multiplied by the attempt count.

Exam Trap

Treating a 200 as the end of the risk on a streamed request, and wrapping only the call in error handling.

A streaming response begins with a 200 and can still carry an error frame mid-stream, such as an overloaded_error during high usage. Handling that wraps the call but not the iteration produces a silently truncated answer with no exception raised.

Practice Scenario

A coppersmith's trade counter is adding a Claude-backed assistant that drafts a stock-enquiry reply while the counter clerk 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 counter browser must hold a websocket open to Anthropic so the clerk 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

Build Exercise

Read the Wire, Then Take the SDK Away and Give It Back

Difficulty
45 minutes

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 compose into worst-case wall clock
  • 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
  1. 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, satisfy yourself they are the same document, then list every line of code the SDK version did not need

    Why: The abstraction only stops feeling like magic 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.

  2. 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.

  3. 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 recovery selection is built on, and guessing it is how retry storms start.

    You should see: BadRequestError and AuthenticationError raised immediately; RateLimitError and 5xx retried before raising.

  4. 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.

  5. 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.

  6. 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 item 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 anywhere near api.anthropic.com.

  7. 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 stem 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
  • 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