Set Custom Headers

Su Suriya Ravichandran Updated 10 Nov 2026
6 min read ·Lesson 160 of 200

Telling the Service More About the Request

Most of what a request says is in its body: the model, the messages, the tools, and the limits. Some information belongs in headers instead. Headers describe the request rather than its content, and the API itself uses several of them. The anthropic-version header selects the API version, and the anthropic-beta header opts a request into features that are not yet part of the standard interface. Applications can add their own headers too, for tracing, routing, or identifying the calling service.

This lesson covers how headers are set at two levels, how per-request values override defaults, which headers the API defines and what they control, and how to validate header values so a mistake does not produce a malformed request. It ends with the rule that matters most: headers are sent over the network, and they must never carry secrets that the application would not want seen.


Two Levels: Client and Request

Headers can be set once for every request, or for a single call:

from anthropic import Anthropic

client = Anthropic(default_headers={"x-service-name": "course-assistant"})

response = client.with_options(
    default_headers={"x-job-id": "job-7781"},
).messages.create(...)

The client-level headers apply to every call made with that client, and the request-level headers are added for one call. When the same name appears at both levels, the request-level value wins for that call. This is the same override rule the SDK uses for timeouts (Lesson 144): defaults give the baseline, and the specific call gives the exception.

The request-level headers can also be passed directly to a call, using the SDK's extra_headers argument on the method. The reference for the version in use shows the exact argument names, and the pattern is the same as with_options.


Headers the API Defines

Some headers change how the API treats a request, and they should be set deliberately:

HeaderPurpose
anthropic-versionSelects the version of the API the request uses. The SDK sets it, and changing it changes behavior.
anthropic-betaOpts into beta features. Several betas are given as a comma-separated list.
anthropic-workspace-idChooses a workspace when a key can act in more than one (Lesson 141's workspace notes)

The beta header is the one most often set by hand. Some features, such as the extended output option for batches (Lesson 140), are available only when their beta identifier is included, and the identifier is given in the feature's documentation. Lists of betas are joined with commas, which is the format the documentation specifies:

headers = {"anthropic-beta": "feature-one-2026-01-01,feature-two-2026-02-01"}

The beta names here are placeholders, to show the format. The real identifiers come from the documentation of each feature, and they change, so they should be copied from there rather than remembered.

Setting anthropic-version by hand is rarely needed, and changing it can change the response format, so it should be done only when a specific version is deliberately required and tested.


Custom Headers for Tracing

Applications often add headers that the API ignores but that other systems read: a service name, a job identifier, or a trace identifier. A proxy, gateway, or log pipeline in front of the API can use them to group traffic. Custom header names conventionally start with x-, and the values should be short, plain text. They identify the caller, not the content, which keeps them safe to log.

The trace identifiers from Lesson 148 are a natural fit. Sending the application's trace identifier as a header lets infrastructure between the application and the API correlate its records with the same operation.


Validating Header Values

Header values are plain text sent over HTTP, and some characters are not allowed. A newline inside a value can split a request into two, a bug that becomes a security issue when the value comes from user input. The validator below enforces a simple rule set, and it also refuses headers that look like credentials:

"""headers_check.py: validate and merge request headers before sending."""

import re

NAME_PATTERN = re.compile(r"^[a-z0-9-]+$")
FORBIDDEN_MARKERS = ("api-key", "authorization", "cookie", "secret", "token")


def problems_with(name: str, value: str) -> list[str]:
    problems = []
    lowered = name.lower()
    if not NAME_PATTERN.match(lowered):
        problems.append(f"header name {name!r} may contain only letters, digits, and hyphens")
    if any(marker in lowered for marker in FORBIDDEN_MARKERS):
        problems.append(f"header {name!r} looks like a credential and must not be set here")
    if "\n" in value or "\r" in value:
        problems.append(f"header {name!r} must not contain line breaks")
    if len(value) > 200:
        problems.append(f"header {name!r} is longer than 200 characters")
    return problems


def merge_headers(defaults: dict, per_request: dict) -> dict:
    merged = dict(defaults)
    merged.update(per_request)
    return merged


if __name__ == "__main__":
    defaults = {"x-service-name": "course-assistant", "x-environment": "staging"}
    per_request = {"x-job-id": "job-7781", "x-environment": "production"}
    print("merged:", merge_headers(defaults, per_request))

    tests = [
        ("x-job-id", "job-7781"),
        ("x-job id", "bad name"),
        ("x-api-key", "placeholder"),
        ("x-note", "line one\nline two"),
    ]
    for name, value in tests:
        print(f"{name!r}:", problems_with(name, value) or "ok")

Output:

merged: {'x-service-name': 'course-assistant', 'x-environment': 'production', 'x-job-id': 'job-7781'}
'x-job-id': ok
'x-job id': ["header name 'x-job id' may contain only letters, digits, and hyphens"]
'x-api-key': ["header 'x-api-key' looks like a credential and must not be set here"]
'x-note': ["header 'x-note' must not contain line breaks"]

How It Works

  1. Per-request values override defaults. The merged result keeps x-service-name from the defaults, and takes x-environment from the per-request value, which is production, as the rule says.
  2. Names are restricted to a safe set, so a name containing a space is rejected before it can cause a malformed request.
  3. Credential-like names are refused. A header named x-api-key is rejected even though its name is well formed, because the API key belongs in the client configuration and the environment, never in a per-request header that is easy to log.
  4. Line breaks are refused, since a newline in a value can split the request, which is a known way to inject headers.
  5. Length is bounded, so an unexpectedly large value, perhaps from user input, cannot inflate every request.
  6. The message quotes are printed by Python's list display. Each message contains single quotes around the header name, so the list shows the message in double quotes. The text is the same either way; only the display differs.

Headers Are Not Secret

Headers are visible to every system between the application and the API. Proxies, logging middleware, and debugging tools can all read them, and some of them record headers by default. A header is therefore a poor place for anything confidential. The API key is set on the client, and it is handled by the SDK. Custom headers should carry identifiers and labels, and anything that could identify a person should be left out unless the destination is trusted with it.


Summary

Headers describe a request, separately from its body, and they can be set on a client for every call or on a single call, with the single call winning. The API defines a few headers that change its behavior, including the beta opt-in, and custom headers carry tracing and routing information. Values must be validated, because line breaks and credential-like names are both avoidable mistakes. And since headers travel openly, they must carry labels and identifiers, never secrets. This completes the Advanced Features section, and the next section covers cloud platforms.

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 Set Custom Headers and get answers drawn from it.

Signed-in readers only.