Inspect Message IDs

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 29 of 200

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:

PrefixIdentifiesWhere you find itLesson
msg_An assistant message (a response)message.idThis lesson
req_An HTTP request to the APImessage._request_id, the request-id header, error bodiesThis lesson, 49
toolu_A tool call in a responseblock.id on tool_use blocks85–87
srvtoolu_A server-side tool call, such as web searchblock.id on server_tool_use blocks155
msgbatch_A Message Batchbatch.id131–140
file_A file uploaded with the Files APIfile.id115–116
wrkspc_A workspace in your organizationConsole, anthropic-workspace-id header6

Message ID vs. Request ID

The two IDs you will use most often are easy to confuse:

Message ID (msg_...)Request ID (req_...)
IdentifiesThe reply Claude generatedThe HTTP request that was sent
Exists for failed requestsNo: a failed request produces no messageYes: errors also have a request ID
Access in Pythonmessage.idmessage._request_id, or from the error's response headers
Main useLinking replies to your application's dataInvestigating 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) or TEXT, 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

  1. 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.
  2. 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.
  3. {**base, ...} merges the shared fields (time, user, question) into each record, so the success and error records have a consistent structure.
  4. record_feedback() stores ratings that reference a message ID, the link between user opinions and specific replies.
  5. find_exchange() scans the log for a message ID. A real application would use a database with an index on message_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.id and message._request_id for 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

  1. Send three requests and print each message.id and message._request_id. Confirm that every value is different.
  2. Trigger an error with an invalid model name and log its request ID using error.response.headers.get("request-id").
  3. Extend find_exchange() to return the feedback records for a message as well.
  4. Write a function feedback_summary() that reads feedback.jsonl and returns the number of up and down ratings.
Show answers for steps 3 and 4

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))

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Inspect Message IDs and get answers drawn from it.

Signed-in readers only.