Inspect Message IDs
What a Message ID Is
Every response from the Messages API has a unique identifier in its id field:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(message.id) # msg_01XFDUDYJgAACzvnptvVoYEL
A message ID is a string that uniquely identifies one assistant reply. It starts with the prefix msg_, followed by a random-looking sequence of characters.
On its own, an ID does nothing: the API does not let you look up a past message by its ID. Its value comes from what your application does with it: logging, linking feedback, debugging, de-duplicating, and connecting related records. This lesson explains message IDs, the other IDs you will meet, and how to use them well.
The IDs You Will See
The Claude API uses several kinds of IDs, each recognizable by its prefix:
| Prefix | Identifies | Where you find it | Lesson |
|---|---|---|---|
msg_ | An assistant message (a response) | message.id | This lesson |
req_ | An HTTP request to the API | message._request_id, the request-id header, error bodies | This lesson, 49 |
toolu_ | A tool call in a response | block.id on tool_use blocks | 85–87 |
srvtoolu_ | A server-side tool call, such as web search | block.id on server_tool_use blocks | 155 |
msgbatch_ | A Message Batch | batch.id | 131–140 |
file_ | A file uploaded with the Files API | file.id | 115–116 |
wrkspc_ | A workspace in your organization | Console, anthropic-workspace-id header | 6 |
Message ID vs. Request ID
The two IDs you will use most often are easy to confuse:
Message ID (msg_...) | Request ID (req_...) | |
|---|---|---|
| Identifies | The reply Claude generated | The HTTP request that was sent |
| Exists for failed requests | No: a failed request produces no message | Yes: errors also have a request ID |
| Access in Python | message.id | message._request_id, or from the error's response headers |
| Main use | Linking replies to your application's data | Investigating issues with Anthropic support |
Because a failed request has a request ID but no message ID, log both for successful responses, and the request ID for errors.
IDs Are Opaque
Treat every ID as an opaque string: a value you store and compare, but never interpret.
- Do not parse it. The characters after the prefix carry no meaning your code should depend on. They are not timestamps or counters.
- Do not assume a fixed length or format. Store IDs in text columns that are wide enough, for example
VARCHAR(255)orTEXT, not a tightly sized field. - Do not generate them yourself. Only the API creates them.
- Use the prefix only for human readability, for example to recognize what kind of ID appears in a log line.
Why this matters: the API can change the internal format of IDs without notice, as long as they remain unique. Code that only stores and compares IDs keeps working. Code that parses them breaks.
No Retrieval by ID
The Messages API is stateless (Lesson 21). There is no endpoint to fetch a past message by its ID. If you need a reply's content later, for history, auditing, or display, you must store it yourself when you receive it.
record = {
"message_id": message.id,
"request_id": message._request_id,
"model": message.model,
"content": message.to_dict()["content"],
"usage": message.usage.to_dict(),
}
to_dict() converts the SDK objects into plain dictionaries that can be stored as JSON.
Practical Uses of Message IDs
1. Linking User Feedback
When users rate an answer, with a thumbs up or down, attach the message ID. You can later find exactly which reply, prompt, and model produced good or bad results:
feedback = {"message_id": "msg_01XF...", "rating": "down", "comment": "Wrong command"}
2. Correlating Logs
Log the message ID and request ID with every exchange. When something goes wrong, you can search all logs for one ID and see the full story: the user, the request, the response, and any follow-up actions.
3. De-duplicating Stored Replies
If a retry or a double-click causes the same reply to be stored twice, the message ID identifies the duplicate. A unique constraint on the column prevents duplicates at the database level:
# Django model field
message_id = models.CharField(max_length=255, unique=True)
4. Supporting Investigations
When contacting Anthropic support about a specific response, provide the request ID (req_...). It identifies the exact request on Anthropic's side.
Request IDs for Errors
Failed requests do not return a Message, but they still have a request ID. The SDK exceptions for HTTP errors include the HTTP response, so you can read the header:
import anthropic
try:
message = 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"Request failed with {error.status_code} (request ID: {request_id})")
Request failed with 404 (request ID: req_011CSHoEeqs5C35K2UUqR7Fy)
error.response is the HTTP response that carried the error, and .headers.get("request-id") reads the ID from its headers. Connection errors (APIConnectionError) have no response, because the request never reached the API, so they have no request ID.
Practical Example: An Exchange Log with IDs
This example records every exchange as one line of JSON in a JSONL file (JSON Lines: one JSON object per line). It includes the IDs, links user feedback to message IDs, and looks up a stored exchange by its ID.
"""exchange_log.py: log exchanges with their IDs, record feedback, and look records up."""
import json
from datetime import datetime, timezone
from pathlib import Path
import anthropic
from anthropic import Anthropic
from anthropic.types import Message
LOG_FILE = Path("exchanges.jsonl")
FEEDBACK_FILE = Path("feedback.jsonl")
client = Anthropic()
def get_text(message: Message) -> str:
return "".join(block.text for block in message.content if block.type == "text").strip()
def append_jsonl(path: Path, record: dict) -> None:
with path.open("a", encoding="utf-8") as file:
file.write(json.dumps(record, ensure_ascii=False) + "\n")
def ask_and_log(question: str, user_id: str) -> Message | None:
base = {"time": datetime.now(timezone.utc).isoformat(), "user_id": user_id, "question": question}
try:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": question}],
)
except anthropic.APIStatusError as error:
append_jsonl(LOG_FILE, {**base, "status": "error", "http_status": error.status_code,
"request_id": error.response.headers.get("request-id")})
return None
append_jsonl(LOG_FILE, {
**base,
"status": "ok",
"message_id": message.id,
"request_id": message._request_id,
"model": message.model,
"stop_reason": message.stop_reason,
"answer": get_text(message),
"usage": {"input": message.usage.input_tokens, "output": message.usage.output_tokens},
})
return message
def record_feedback(message_id: str, rating: str, comment: str = "") -> None:
append_jsonl(FEEDBACK_FILE, {"message_id": message_id, "rating": rating, "comment": comment})
def find_exchange(message_id: str) -> dict | None:
if not LOG_FILE.exists():
return None
with LOG_FILE.open(encoding="utf-8") as file:
for line in file:
record = json.loads(line)
if record.get("message_id") == message_id:
return record
return None
if __name__ == "__main__":
reply = ask_and_log("Which command creates a Django app?", user_id="student-42")
if reply:
print(f"Answer: {get_text(reply)}")
print(f"IDs: message={reply.id} request={reply._request_id}")
record_feedback(reply.id, rating="up", comment="Clear and correct")
stored = find_exchange(reply.id)
print(f"Found stored exchange: {stored['question']!r} -> stop_reason={stored['stop_reason']}")
Example output:
Answer: Run `python manage.py startapp <app_name>` inside your project folder.
IDs: message=msg_01Q8Faay6S7QPTvEUUQARt7h request=req_018EeWyXxfu5pfWkrYcMdjWG
Found stored exchange: 'Which command creates a Django app?' -> stop_reason=end_turn
A line in exchanges.jsonl:
{"time": "2026-11-04T10:15:02+00:00", "user_id": "student-42", "question": "Which command creates a Django app?", "status": "ok", "message_id": "msg_01Q8Faay6S7QPTvEUUQARt7h", "request_id": "req_018EeWyXxfu5pfWkrYcMdjWG", "model": "claude-opus-5-5", "stop_reason": "end_turn", "answer": "Run `python manage.py startapp <app_name>` ...", "usage": {"input": 15, "output": 92}}
How It Works
append_jsonl()opens the file in append mode ("a") and writes one JSON object per line. JSONL is easy to append to, easy to read line by line, and easy to load into analysis tools.ask_and_log()logs every request. Successful responses record the message ID, request ID, model, stop reason, answer, and usage. Errors record the HTTP status and the request ID from the response headers.{**base, ...}merges the shared fields (time, user, question) into each record, so the success and error records have a consistent structure.record_feedback()stores ratings that reference a message ID, the link between user opinions and specific replies.find_exchange()scans the log for a message ID. A real application would use a database with an index onmessage_id. The JSONL scan shows the principle.
Warning: Logs that contain questions and answers may include personal or sensitive information. Apply your data-retention and privacy policies to them, restrict access, and consider storing only IDs and metadata when the full text is not needed.
Common Mistakes
Logging Only the Message ID
What happens: failed requests leave no trace, and support investigations lack the request ID.
Fix: log the request ID for every request, plus the message ID for successful ones.
Parsing or Generating IDs
What happens: code breaks when the ID format changes, or fake IDs cause confusion.
Fix: treat IDs as opaque strings that only the API creates.
Expecting to Retrieve Messages by ID
What happens: the application stores only IDs and later finds there is no way to get the content back.
Fix: store the content you need when you receive the response.
Using Too-Short Database Columns
What happens: IDs are truncated or rejected when their length exceeds the column size.
Fix: use generously sized text columns for IDs.
Confusing Message IDs and Request IDs in Support Tickets
Fix: give support the req_... request ID.
Best Practices
- Log
message.idandmessage._request_idfor every successful exchange, and the request ID for every error. - Store reply content yourself if you will need it later.
- Treat all IDs as opaque strings with generous storage sizes.
- Link feedback, analytics, and audit records to message IDs.
- Use unique constraints on message IDs to prevent duplicate storage.
- Protect logs that contain conversation text.
Exercise
- Send three requests and print each
message.idandmessage._request_id. Confirm that every value is different. - Trigger an error with an invalid model name and log its request ID using
error.response.headers.get("request-id"). - Extend
find_exchange()to return the feedback records for a message as well. - Write a function
feedback_summary()that readsfeedback.jsonland returns the number ofupanddownratings.
Step 3:
def find_feedback(message_id: str) -> list[dict]:
if not FEEDBACK_FILE.exists():
return []
with FEEDBACK_FILE.open(encoding="utf-8") as file:
return [record for line in file if (record := json.loads(line))["message_id"] == message_id]
def find_exchange_with_feedback(message_id: str) -> dict | None:
exchange = find_exchange(message_id)
if exchange is not None:
exchange["feedback"] = find_feedback(message_id)
return exchange
The walrus operator (:=) parses each line once and reuses the result in the condition and the list.
Step 4:
from collections import Counter
def feedback_summary() -> dict[str, int]:
if not FEEDBACK_FILE.exists():
return {}
with FEEDBACK_FILE.open(encoding="utf-8") as file:
return dict(Counter(json.loads(line)["rating"] for line in file))