Read Raw Responses
Looking Beneath the Parsed Message
Every example so far has received a parsed message: an object with content blocks, a stop reason, and usage. That object is the convenient view of a response, and it hides the envelope the response arrived in. The envelope holds the HTTP status, the headers, and the request ID, along with the body. Most applications never need those parts. Some need them badly: a rate-limit dashboard reads headers, a support workflow records request IDs, and a debugging session wants to see exactly what the server sent.
The SDK provides a raw response interface for this. It returns the same data as a normal call, wrapped so the envelope is available, and the parsed message is produced only when you ask for it. This lesson explains how to use that wrapper, which headers are worth reading, and how to record them without leaking anything sensitive.
The Raw Response Wrapper
A normal call returns the parsed message directly. A raw call goes through a different entry point, with_raw_response, and returns a wrapper:
raw = client.messages.with_raw_response.create(
model="claude-opus-5-5",
max_tokens=256,
messages=[{"role": "user", "content": "Say hello."}],
)
message = raw.parse() # The parsed message, as usual
request_id = raw.headers.get("request-id") # The envelope, available before or after parsing
The wrapper exposes headers, which behave like a dictionary, and parse(), which returns the message object. The same data is available either way, so the choice is about what the code needs to read. Calling parse() is what gives you the typed message, and reading headers does not require parsing first.
The documentation's naming for these entry points may differ slightly between SDK versions, and the SDK reference should be checked for the exact methods of the version in use. The pattern, a wrapper with headers and a parse step, is the same across versions.
Which Headers Matter
A response carries many headers, and only a few help with reliability and support:
| Header | Use |
|---|---|
request-id | The identifier support teams need (Lesson 148) |
| Rate-limit headers | Remaining allowance, for pacing and dashboards (Lesson 142) |
retry-after | The wait the service asks for, on rate-limited responses (Lesson 142) |
| Model or version headers | Confirming which model version served the request |
The exact names of the rate-limit headers are listed in the rate limits documentation, and they should be copied from there. A dashboard should read them from every response, not only from failures, because the trend in remaining allowance is what warns of trouble before a 429 arrives.
Recording Headers Safely
Headers can contain values that must never be logged. Authentication information, session cookies, and other credentials may appear in some responses or in requests, and a log that records every header is a leak waiting to happen. The safe approach records an explicit allowlist of headers, and drops everything else:
"""raw_headers.py: read a raw response, and record only an allowlist of headers."""
SAFE_HEADERS = {"request-id", "retry-after", "content-type"}
SENSITIVE_MARKERS = ("authorization", "cookie", "set-cookie", "api-key")
def safe_header_summary(headers: dict) -> dict:
summary = {}
for name, value in headers.items():
key = name.lower()
if any(marker in key for marker in SENSITIVE_MARKERS):
continue
if key in SAFE_HEADERS:
summary[key] = value
return summary
class FakeRawResponse:
"""Stands in for the SDK's wrapper, so the example runs offline."""
def __init__(self, headers: dict, body: dict):
self.headers = headers
self._body = body
def parse(self) -> dict:
return self._body
if __name__ == "__main__":
raw = FakeRawResponse(
headers={"request-id": "req_0198", "content-type": "application/json",
"retry-after": "2", "set-cookie": "session=abc123",
"x-api-key-echo": "sk-demo-not-real"},
body={"id": "msg_01", "stop_reason": "end_turn"},
)
print("summary:", safe_header_summary(raw.headers))
print("parsed stop reason:", raw.parse()["stop_reason"])
Output:
summary: {'request-id': 'req_0198', 'content-type': 'application/json', 'retry-after': '2'}
parsed stop reason: end_turn
How It Works
- The summary is an allowlist. Only the three headers named in
SAFE_HEADERScan appear in the output. Anything not on the list is dropped, including headers that a future version of the service might add. - Sensitive names are removed even if they are allowed by mistake. The second filter drops any header whose name contains a credential marker, such as cookies or key material. Two layers protect against two kinds of error: an unknown sensitive header, and an allowed header that turns out to be sensitive.
- The header names are compared in lowercase. HTTP header names are case-insensitive, so the check must be too, or
Request-IDwould slip past a comparison againstrequest-id. - The parsed body is still available after the headers have been read. The wrapper separates the two, so logging does not require the body, and the body does not need to be logged to find the request ID.
- The fake key value in the example shows that a header carrying key-like material is excluded. The example uses an obviously fake value, so no real credential appears in the material.
When Raw Access Is Worth It
Raw access is a tool for specific jobs, and most code does not need it. It is worth using when a dashboard reads rate-limit headers from every call, when a support workflow must store the request ID before parsing could fail, or when an investigation requires the exact status and headers of a problematic response. For everyday application code, the parsed message and the error object already carry what is needed.
A subtle point follows from this. Reading the raw response is itself a choice that affects what gets logged, and the allowlist discipline from Lesson 147 applies with extra force, because the envelope contains more than the body does.
Streaming Responses
Streaming has its own envelope. The equivalent entry point, with_streaming_response, provides headers before the stream begins, which lets an application read the request ID and rate-limit state early, and then consume the events. Lesson 53 covered the events themselves. The documentation for streaming gives the exact context-manager form, and that form should be copied from the SDK reference, since it involves resource handling that must close properly.
Summary
The raw response wrapper exposes the HTTP envelope around a message: the headers, and a parse step that produces the typed object. Headers carry the request ID, the rate-limit state, and the retry instruction, which are valuable for reliability and support. Recording them requires an allowlist, because the envelope can contain credentials that must never reach a log. The next lesson completes the section with the other side of the request: setting headers that the application sends.