The count_tokens Endpoint in the Claude API
8 min read · updated August 11, 2026
Claude’s tokenizer is not available locally, so the only reliable way to know what a request will cost or whether it will fit is to ask. There is an endpoint for that, it is free of charge, and it takes the same body as the generation call.
What the endpoint does
POST /v1/messages/count_tokens accepts a Messages API request body and returns the number of input tokens that body would be billed for. It does not generate anything, it does not consume output tokens, and Anthropic documents it as not billed — though it is subject to its own rate limits, so it is not free of consequence in a tight loop.
The design decision that makes it useful is that it takes the whole body: system, messages, tools, image blocks, document blocks. That is what separates it from a tokenizer. A tokenizer counts a string; this counts a request, including the parts of a request that you did not write and are still charged for.
Three questions it answers, in rough order of how often they come up. Will this request fit inside the context window — which you need before sending, because the alternative is a 400 you have to recover from with a user waiting. What will this cost — for a per-customer quota, a spend estimate shown in a UI, or a decision about which model to route to. And where are the tokens going — the diagnostic use, where you count the same body twice with one part removed and read the difference.
The request
It takes the same three headers as any Messages call. Note what is not present: there is no max_tokens, because nothing is being generated — which makes this the one Messages-shaped endpoint where the usual max_tokens requirement does not apply.
curl https://api.anthropic.com/v1/messages/count_tokens \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5-20250929",
"system": "You are a support triage assistant. Classify each ticket.",
"tools": [
{
"name": "record_triage",
"description": "Record the triage decision for a ticket.",
"input_schema": {
"type": "object",
"properties": {
"severity": {"type": "string", "enum": ["low", "medium", "high"]},
"team": {"type": "string"},
"summary": {"type": "string"}
},
"required": ["severity", "team", "summary"]
}
}
],
"messages": [
{"role": "user", "content": "Checkout returns a 500 for EU cards since 09:14 UTC."}
]
}'The response
One field:
{"input_tokens": 403}That number is directly comparable to the usage.input_tokens you will see on the real generation response for the same body, which is the property that makes it worth trusting. If they disagree, the bodies differed — almost always because something was added between the count and the send.
What it cannot tell you is output. Output length is not known until it is generated, and it is often the larger half of the bill, since output tokens are priced several times higher than input on every Claude tier. Budget it from max_tokens as a worst case and from your observed usage.output_tokens as a typical case.
It also does not break the number down. A single integer for a body containing a system prompt, six tool definitions, a conversation and an image is not much of a diagnosis on its own. The way to get a breakdown is subtraction: count the full body, count it again with tools omitted, and the difference is what your tool definitions cost on every request. Repeat for system and for the document blocks. Four calls, no billing, and an itemised answer to “why is this request 8,000 tokens when the user typed nine words”.
One caveat on interpreting the number against a bill: with prompt caching in play, the count is the number of input tokens in the request, not the number charged at the full input rate. Cache reads are billed at a reduced rate and cache writes at a premium, so a cached request’s cost is not the count multiplied by the base price. The split appears on the real response in the cache-related usage fields, not here.
Building the guard
The useful pattern is one function that assembles the body, and two calls against that same object — so the thing counted and the thing sent cannot drift.
- Build the body once.
import anthropic client = anthropic.Anthropic() MODEL = "claude-sonnet-4-5-20250929" WINDOW = 200_000 MAX_OUT = 4096 def build(history, question, documents): return { "model": MODEL, "system": SYSTEM_PROMPT, "tools": TOOLS, "messages": [*history, {"role": "user", "content": [*documents, {"type": "text", "text": question}]}], } - Count it.
body = build(history, question, documents) count = client.messages.count_tokens(**body).input_tokens
- Check it against the window, reserving the output. The context window holds input and output together, so the input budget is the window minus what you are reserving with
max_tokens, minus a margin.budget = WINDOW - MAX_OUT - 1_000 # 1,000 tokens of headroom while count > budget and len(history) > 2: history = history[2:] # drop the oldest exchange body = build(history, question, documents) count = client.messages.count_tokens(**body).input_tokens - Send the same object.
response = client.messages.create(max_tokens=MAX_OUT, **body) print(response.usage.input_tokens, response.usage.output_tokens)
- Log both numbers. The counted number and the billed number, so a divergence shows up as data rather than as a surprise invoice.
The loop above trims two messages at a time because dropping one leaves a dangling assistant turn without its user turn. Trimming a conversation is the naive strategy and it is fine up to a point; beyond it, summarise the dropped turns into a single message and keep that instead, so the model does not lose the beginning of the conversation entirely.
Two properties of that structure are doing the real work, and both are easy to lose in a refactor. The body is built by one function and passed to both calls as the same object, so there is no path by which the counted request and the sent request can differ — the usual bug here is a version that counts a string and then sends a body assembled somewhere else, which drifts the moment anybody adds a tool. And the budget subtracts max_tokens before comparing, because the window holds input and output together; a guard that compares the input count against the raw window size passes and then fails at the API.
The thousand-token margin is not superstition either. Between counting and sending, a middleware can append a line to the system prompt, a retrieval step can return a slightly larger chunk on a retry, or a tool list can be extended by a feature flag. The margin absorbs that. Size it to the largest thing your own request pipeline can add after the count, and if the answer is “nothing can be added after the count”, you have a better guarantee than the margin gives you.
Where the number goes wrong
- Omitting
tools. The most common cause of a count that is thousands of tokens too low. Tool definitions are part of the prompt. - Omitting
system. Same failure, smaller magnitude, and easier to miss because the SDK will happily accept a body without it. - Counting a different model. The count is model-specific. Counting against a small model and sending to a large one is not guaranteed to give the same answer.
- Counting per chunk in a hot loop. This is a network call with its own rate limit. For high-volume chunking, estimate locally and use
count_tokensat the assembly boundary, where there is one call per request rather than one per chunk. - Forgetting the output reservation. A body that counts at 199,000 tokens against a 200,000-token window still fails once you ask for 4,096 tokens of output. That failure, and its error body, is on the context window exceeded page.
The rate-limit point is the one that decides the architecture of a high-volume system. count_tokens is free of charge and not free of quota, so a design that calls it once per document in a hundred-thousand-document ingest is a design that will be throttled. The workable pattern is a two-tier one: estimate locally with a cheap heuristic or a GPT tokenizer plus a generous margin, and spend a real count only at the point where the answer changes a decision — the assembled request, the routing threshold, the quota check. That keeps the network calls proportional to requests rather than to chunks.
The other structural mistake is treating the count as a validation gate that either passes or throws. It is more useful as an input to a branch. A request counted at 40,000 tokens might go to a cheaper model; at 180,000 it might trigger the summarisation path; at 240,000 it might be rejected with a message to the user that names the actual limit rather than a generic failure. All three of those are better products than a 400 surfaced as “something went wrong”, and all three need the number before the request, which is the whole reason this endpoint exists.