Read Request IDs
What a Request ID Is
Every HTTP request to the Claude API receives a unique request ID, a string that starts with req_:
req_018EeWyXxfu5pfWkrYcMdjWG
The API returns it in the request-id response header for every response, both successful and failed. It identifies the exact request on Anthropic's side, which makes it the single most useful value when you investigate a problem or contact support.
Lesson 29 compared request IDs with message IDs. This lesson focuses on reading request IDs in every situation (success, raw responses, and errors) and on making them part of your logging.
Why Request IDs Matter
| Situation | How the request ID helps |
|---|---|
| A user reports a strange or wrong answer | Find the exact request in your logs, with its prompt, model, and usage |
| A request failed with an error | Give the request ID to Anthropic support so they can investigate that request |
| Intermittent failures | Group and compare failing requests by ID, time, and error type |
| Tracing across systems | Connect your application's logs, monitoring, and user reports to one specific API call |
A message ID (msg_…) exists only for successful responses. A request ID exists for every request that reached the API, including failures. That is why it is the better value for error investigation.
Reading the Request ID from a Successful Response
The SDK attaches the request ID to response objects as _request_id:
from anthropic import Anthropic
client = Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG
Although the name begins with an underscore, _request_id is a public, documented property. The underscore only avoids clashing with field names from the API response.
Reading It from Raw Responses
with_raw_response gives access to all HTTP headers (Lesson 14), including the request ID and other useful metadata:
raw = client.messages.with_raw_response.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(raw.headers.get("request-id")) # req_...
print(raw.headers.get("anthropic-organization-id")) # Your organization
print(raw.headers.get("anthropic-workspace-id")) # The workspace that ran the request
message = raw.parse() # The usual Message object
| Header | Meaning |
|---|---|
request-id | Unique ID of this request |
anthropic-organization-id | The organization the API key belongs to |
anthropic-workspace-id | The workspace the request ran in |
Rate-limit headers are also available here. Lesson 142 uses them.
Reading It from Errors
Failed requests do not return a Message, but errors that came from the API still carry the request ID. For APIStatusError and its subclasses, such as RateLimitError, BadRequestError, and AuthenticationError, read it from the HTTP response attached to the exception:
import anthropic
try:
client.messages.create(
model="claude-does-not-exist",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
except anthropic.APIStatusError as error:
request_id = error.response.headers.get("request-id")
print(f"HTTP {error.status_code}, request ID: {request_id}")
HTTP 404, request ID: req_011CSHoEeqs5C35K2UUqR7Fy
The JSON error body also contains a request_id field (Lesson 2), so the ID is available even if you only have the body.
Errors Without a Request ID
APIConnectionError and APITimeoutError mean the request never received a response, for example because of a network failure. They have no HTTP response and therefore no request ID. For these errors, log the time, the operation, and the error message instead.
| Error type | Request ID available? | Where |
|---|---|---|
| Successful response | Yes | message._request_id |
APIStatusError and subclasses (4xx/5xx) | Yes | error.response.headers.get("request-id") |
APIConnectionError, APITimeoutError | No | — |
Request IDs in Logs
Request IDs are most valuable when they are automatically written to your logs with every API call. A good log line for an API call includes:
| Field | Example |
|---|---|
| Time | 2026-11-05T10:15:02Z |
| Your own operation or feature name | summarize_ticket |
| Your user or session identifier (non-sensitive) | user=4821 |
| Request ID | req_018EeWyX… |
| Outcome | ok, error 429, connection_error |
| Model, stop reason, and usage (on success) | claude-opus-5-5, end_turn, in=320 out=145 |
| Duration | 2.4s |
With these fields, any problem report can be traced to the exact request, and any request can be traced back to the feature and user involved.
Practical Example: A Wrapper That Logs Every Request ID
This wrapper sends a request and writes one structured log line for every outcome, whether success, API error, or connection error, always including the request ID when one exists. It also produces a ready-to-send support summary for failed requests.
"""request_logging.py: log request IDs for every API call outcome."""
import json
import logging
import time
import anthropic
from anthropic import Anthropic
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("claude")
client = Anthropic()
def log_event(**fields) -> None:
logger.info(json.dumps(fields, ensure_ascii=False))
def call_claude(operation: str, user_id: str, **request):
start = time.perf_counter()
base = {"operation": operation, "user": user_id, "model": request.get("model")}
try:
message = client.messages.create(**request)
except anthropic.APIStatusError as error:
request_id = error.response.headers.get("request-id")
log_event(**base, outcome=f"error_{error.status_code}", request_id=request_id,
error=error.message, seconds=round(time.perf_counter() - start, 2))
raise
except anthropic.APIConnectionError as error:
log_event(**base, outcome="connection_error", request_id=None,
error=str(error), seconds=round(time.perf_counter() - start, 2))
raise
log_event(**base, outcome="ok", request_id=message._request_id, message_id=message.id,
stop_reason=message.stop_reason, input_tokens=message.usage.input_tokens,
output_tokens=message.usage.output_tokens,
seconds=round(time.perf_counter() - start, 2))
return message
def support_summary(error: anthropic.APIStatusError) -> str:
"""Text to include when contacting Anthropic support about a failed request."""
return (f"Request ID: {error.response.headers.get('request-id')}\n"
f"HTTP status: {error.status_code}\n"
f"Error: {error.message}")
if __name__ == "__main__":
call_claude("explain_term", "user-4821",
model="claude-opus-5-5", max_tokens=1024,
messages=[{"role": "user", "content": "What is a REST API? One sentence."}])
try:
call_claude("explain_term", "user-4821",
model="claude-does-not-exist", max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}])
except anthropic.APIStatusError as error:
print("\n--- Support summary ---")
print(support_summary(error))
Example output:
{"operation": "explain_term", "user": "user-4821", "model": "claude-opus-5-5", "outcome": "ok", "request_id": "req_018EeWyXxfu5pfWkrYcMdjWG", "message_id": "msg_01Q8Faay6S7QPTvEUUQARt7h", "stop_reason": "end_turn", "input_tokens": 17, "output_tokens": 64, "seconds": 2.1}
{"operation": "explain_term", "user": "user-4821", "model": "claude-does-not-exist", "outcome": "error_404", "request_id": "req_011CSHoEeqs5C35K2UUqR7Fy", "error": "...", "seconds": 0.3}
--- Support summary ---
Request ID: req_011CSHoEeqs5C35K2UUqR7Fy
HTTP status: 404
Error: ...
How It Works
log_event()writes each event as one line of JSON. Structured logs like this can be searched and filtered by field in log tools, for example "all events whereoutcomestarts witherror_".call_claude()wrapsmessages.create()and logs exactly one event per call, whatever the outcome.- API errors (
APIStatusError) are logged with the HTTP status and the request ID from the response headers, then re-raised withraise, so the caller can still handle the error. Logging must not swallow errors. - Connection errors are logged with
request_id=None, which correctly reflects that no request ID exists. - Successful calls log the request ID, message ID, stop reason, usage, and duration (
time.perf_counter()), everything needed to investigate a reply later. support_summary()formats the information support needs. The request ID comes first because it identifies the request.
Note: be careful about what you log. Do not log API keys, and think carefully before logging full prompts or responses, which may contain personal data. Request IDs, message IDs, and usage numbers are safe and usually sufficient.
Common Mistakes
Logging Only Message IDs
What happens: failed requests leave no usable identifier.
Fix: log the request ID for every request, successful or not.
Swallowing Errors While Logging Them
What happens: a wrapper logs an error and returns None, so callers continue as if nothing happened.
Fix: re-raise after logging, or return an explicit error result.
Expecting a Request ID for Connection Errors
What happens: code crashes trying to read headers from an error that has no response.
Fix: read request IDs only from APIStatusError and its subclasses. Handle connection errors separately.
Unstructured Log Messages
What happens: "Error calling Claude: …" lines are hard to search and correlate.
Fix: log structured fields, including request_id, operation, and outcome.
Logging Sensitive Content
What happens: logs become a store of personal data or secrets.
Fix: log identifiers and metadata by default, and only log content deliberately, with access controls and retention limits.
Best Practices
- Log the request ID for every API call, along with the operation, outcome, and duration.
- Read it from
_request_idon success and from the response headers on API errors. - Use structured (JSON) logs so request IDs can be searched.
- Re-raise errors after logging them.
- Include the request ID in support requests, and in internal bug reports.
- Keep logs free of secrets, and minimize personal data.
Exercise
- Use
with_raw_responseto print therequest-id,anthropic-organization-id, andanthropic-workspace-idheaders for a request. - Extend
call_claude()to also catchanthropic.APITimeoutErrorseparately, loggingoutcome="timeout". Where should thatexceptblock go, and why? - Add a
feature_versionfield to every log event, for example the prompt template version from Lesson 34. - Write a function that reads a log file of JSON lines and prints every request ID whose outcome is not
"ok".
Step 2: APITimeoutError is a subclass of APIConnectionError, so its except block must come before the APIConnectionError block. Otherwise the more general block would catch timeouts first.
except anthropic.APITimeoutError as error:
log_event(**base, outcome="timeout", request_id=None,
error=str(error), seconds=round(time.perf_counter() - start, 2))
raise
except anthropic.APIConnectionError as error:
...
Step 4:
import json
def failed_request_ids(path: str) -> list[str]:
ids = []
with open(path, encoding="utf-8") as file:
for line in file:
event = json.loads(line)
if event.get("outcome") != "ok":
ids.append(event.get("request_id") or "(no request ID)")
return ids