Skip to content

Instantly share code, notes, and snippets.

@troykelly
Created March 17, 2026 05:07
Show Gist options
  • Select an option

  • Save troykelly/6fdb3845d3c53a39469c338c125dfff6 to your computer and use it in GitHub Desktop.

Select an option

Save troykelly/6fdb3845d3c53a39469c338c125dfff6 to your computer and use it in GitHub Desktop.
Requisite headers for claude oAuth API requests

Claude Code OAuth & API Proxy Specification

Unofficial complete specification for implementing a proxy that handles Claude Code's OAuth authentication flow and API requests. Captured from Claude Code CLI v2.1.77 on 2026-03-17.


Table of Contents

  1. Architecture Overview
  2. Hosts & Endpoints
  3. OAuth Flow
  4. Post-Auth Initialisation Sequence
  5. Messages API (Inference)
  6. Telemetry & Non-Essential Endpoints
  7. Authentication Patterns
  8. Rate Limit Headers
  9. SSE Streaming Response Format
  10. MCP Server Proxying
  11. Version Check
  12. Full Request Body Reference

Architecture Overview

Claude Code communicates with seven distinct hosts:

Host Purpose Auth Method
claude.ai OAuth authorization (browser-based) None (browser redirect)
platform.claude.com OAuth token exchange None (uses auth code + PKCE)
api.anthropic.com All API calls (profile, messages, telemetry, feature flags, MCP servers) Authorization: Bearer <access_token>
mcp-proxy.anthropic.com Remote MCP server communication Authorization: Bearer <access_token>
storage.googleapis.com Version check None
raw.githubusercontent.com Plugin security list & changelog None
github.com Git operations (cert-pinned, cannot be MITM proxied) None

Optional external telemetry hosts (can be blocked):

Host Purpose
http-intake.logs.us5.datadoghq.com Datadog logging
api.segment.io Segment analytics

A proxy must handle traffic to claude.ai, platform.claude.com, and api.anthropic.com to fully support Claude Code.


Hosts & Endpoints

claude.ai (Browser — OAuth Authorization)

Method Path Purpose
GET /oauth/authorize Initiate OAuth authorization

platform.claude.com (OAuth Token Exchange)

Method Path Purpose
POST /v1/oauth/token Exchange authorization code for tokens / refresh tokens

api.anthropic.com (API)

Method Path Purpose Required
GET /api/oauth/profile User profile & subscription info Yes
GET /api/oauth/claude_cli/roles Organization & workspace roles Yes
GET /api/oauth/account/settings Account feature flags & preferences Yes
GET /api/oauth/usage Current usage / rate limit utilization Yes
GET /api/oauth/claude_cli/client_data Client-specific configuration Yes
GET /api/claude_code_grove Grove (session sharing) status Yes
GET /api/claude_code_penguin_mode Penguin mode (extended thinking) status Yes
POST /api/eval/sdk-{hash} GrowthBook feature flag evaluation Yes
GET /v1/mcp_servers?limit=1000 List remote MCP servers Yes
POST /v1/messages?beta=true Inference (Messages API) Yes
POST /api/event_logging/v2/batch Telemetry events (v2) No (can be stubbed)
POST /api/event_logging/batch Telemetry events (v1, legacy) No (can be stubbed)
POST /api/claude_code/metrics Usage metrics No (can be stubbed)

mcp-proxy.anthropic.com (Remote MCP)

Method Path Purpose
POST /v1/mcp/{server_id} MCP JSON-RPC calls to remote servers

storage.googleapis.com (Version Check)

Method Path Purpose
GET /claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest Check latest CLI version

raw.githubusercontent.com (Plugin Security & Changelog)

Method Path Purpose
GET /anthropics/claude-plugins-official/refs/heads/security/security.json Plugin security blocklist
GET /anthropics/claude-code/refs/heads/main/CHANGELOG.md Changelog for update notifications

No authentication required. Can be stubbed or blocked. The security.json request includes a ?t={timestamp} cache-buster.

Note: github.com is also contacted but uses certificate pinning — it will reject MITM proxy certificates with TLS errors. This is expected and non-fatal.


OAuth Flow

Step 1: Authorization URL (Browser)

Claude Code opens a browser to this URL:

https://claude.ai/oauth/authorize?
  code=true&
  client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&
  response_type=code&
  redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback&
  scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference+user%3Asessions%3Aclaude_code+user%3Amcp_servers+user%3Afile_upload&
  code_challenge={PKCE_CHALLENGE}&
  code_challenge_method=S256&
  state={STATE}

Parameters:

Parameter Value
client_id 9d1c250a-e61b-44d9-88ed-5944d1962f5e (Claude Code app)
response_type code
redirect_uri https://platform.claude.com/oauth/code/callback
scope org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload
code_challenge_method S256 (PKCE)
code_challenge Base64url-encoded SHA-256 of code_verifier
state Random base64url string for CSRF protection

Step 2: User Authorizes in Browser

The user logs in and authorizes the app. The browser redirects to:

https://platform.claude.com/oauth/code/callback?code={AUTH_CODE}#{STATE}

The page displays a code in the format:

{AUTH_CODE}#{STATE}

The user pastes this code back into the CLI. The # separates the authorization code from the state parameter.

Step 3: Token Exchange

POST https://platform.claude.com/v1/oauth/token
Content-Type: application/json
User-Agent: axios/1.13.4

Request Body:

{
  "grant_type": "authorization_code",
  "code": "{AUTH_CODE}",
  "redirect_uri": "https://platform.claude.com/oauth/code/callback",
  "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
  "code_verifier": "{PKCE_CODE_VERIFIER}",
  "state": "{STATE}"
}

Response (200 OK):

{
  "token_type": "Bearer",
  "access_token": "sk-ant-oat01-...",
  "expires_in": 28800,
  "refresh_token": "sk-ant-ort01-...",
  "scope": "user:file_upload user:inference user:mcp_servers user:profile user:sessions:claude_code",
  "organization": {
    "uuid": "aaaaaaaa-...",
    "name": "Organization Name"
  },
  "account": {
    "uuid": "aaaaaaaa-...",
    "email_address": "user@example.com"
  }
}

Key details:

  • Access tokens have prefix sk-ant-oat01-
  • Refresh tokens have prefix sk-ant-ort01-
  • expires_in is 28800 seconds (8 hours)
  • No Authorization header is sent on this request
  • The token exchange request uses User-Agent: axios/1.13.4 (not the claude-cli user agent)

Step 4: Token Refresh

When the access token expires, Claude Code sends:

POST https://platform.claude.com/v1/oauth/token
Content-Type: application/json
{
  "grant_type": "refresh_token",
  "refresh_token": "sk-ant-ort01-...",
  "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
}

Response format is the same as the initial token exchange.


Post-Auth Initialisation Sequence

After obtaining tokens, Claude Code makes these requests before accepting user input. All use Authorization: Bearer {access_token}. Many are fired concurrently.

1. Feature Flags (GrowthBook)

POST https://api.anthropic.com/api/eval/sdk-{hash}
Authorization: Bearer {access_token}
Content-Type: application/json
{
  "attributes": {
    "id": "{device_hash}",
    "sessionId": "{session_uuid}",
    "deviceID": "{device_hash}",
    "platform": "darwin",
    "userType": "external",
    "appVersion": "2.1.77"
  },
  "forcedVariations": {},
  "forcedFeatures": [],
  "url": ""
}

Response: Contains 145+ feature flags. Key ones:

{
  "features": {
    "tengu_streaming_tool_execution2": { "value": true, "on": true, "off": false, "source": "force" },
    "tengu_hawthorn_window": { "value": 200000, "on": true, "off": false, "source": "defaultValue" },
    "tengu_workout2": { "value": true, "on": true, "off": false, "source": "defaultValue" },
    "tengu_accept_with_feedback": { "value": true, "on": true, "off": false, "source": "force" },
    "tengu_quartz_lantern": { "value": true, "on": true, "off": false, "source": "defaultValue" },
    "tengu_sm_compact": { "value": false, "on": false, "off": true, "source": "defaultValue" },
    "tengu_system_prompt_global_cache": { "value": false, "on": false, "off": true, "source": "defaultValue" }
  }
}

Each feature flag has the structure:

{
  "value": true,
  "on": true,
  "off": false,
  "source": "defaultValue",
  "experiment": null,
  "experimentResult": null,
  "ruleId": null
}

2. Account Settings

GET https://api.anthropic.com/api/oauth/account/settings
Authorization: Bearer {access_token}

Response: Large JSON with account preferences, feature toggles, onboarding state.

{
  "enabled_web_search": true,
  "paprika_mode": "off",
  "tool_search_mode": "auto",
  "grove_enabled": false,
  "grove_updated_at": "2026-01-01T01:01:01.012345Z",
  "onboarding_use_case": "personal",
  "has_started_claudeai_onboarding": true,
  "has_finished_claudeai_onboarding": true,
  "enabled_artifacts_attachments": false,
  "dismissed_claudeai_banners": [
    {
      "banner_id": "mcp_directory_chin_14-0-2026",
      "dismissed_at": "2026-01-01T01:01:01.012345Z"
    }
  ],
  "dismissed_saffron_themes": true,
  "internal_tier_org_type": null,
  "internal_tier_rate_limit_tier": null,
  "ccr_sharing_enforce_repo_check": null,
  "ccr_sharing_show_display_name": null,
  "ccr_sharing_auto_share_on_pr": null,
  "ccr_auto_archive_on_pr_close": null,
  "ccr_persistent_memory": null,
  "ccr_plugins_mount": null,
  "voice_preference": null,
  "voice_speed": null
}

3. Grove Status

GET https://api.anthropic.com/api/claude_code_grove
Authorization: Bearer {access_token}
{
  "grove_enabled": true,
  "domain_excluded": false,
  "notice_is_grace_period": false,
  "notice_reminder_frequency": 0
}

4. Penguin Mode

GET https://api.anthropic.com/api/claude_code_penguin_mode
Authorization: Bearer {access_token}
{
  "enabled": true,
  "disabled_reason": null
}

5. Client Data

GET https://api.anthropic.com/api/oauth/claude_cli/client_data
Authorization: Bearer {access_token}
{
  "client_data": {}
}

6. MCP Servers

GET https://api.anthropic.com/v1/mcp_servers?limit=1000
Authorization: Bearer {access_token}
{
  "data": [
    {
      "type": "mcp_server",
      "id": "mcpsrv_01234567890ABCDEFGHIJKLM",
      "display_name": "Gmail",
      "url": "https://gmail.mcp.claude.com/mcp",
      "created_at": "2026-01-01T01:01:01.012345Z"
    },
    {
      "type": "mcp_server",
      "id": "mcpsrv_1234567890ABCDEFGHIJKLMN",
      "display_name": "Google Calendar",
      "url": "https://gcal.mcp.claude.com/mcp",
      "created_at": "2026-01-01T01:01:01.012345Z"
    }
  ],
  "next_page": null
}

7. Preflight Messages Request

Claude Code sends a small "preflight" messages request on startup (before user input):

POST https://api.anthropic.com/v1/messages?beta=true
Authorization: Bearer {access_token}
anthropic-beta: oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05
anthropic-dangerous-direct-browser-access: true
anthropic-version: 2023-06-01
Content-Type: application/json
{
  "model": "claude-haiku-4-5-20251001",
  "max_tokens": 1,
  "messages": [{"role": "user", "content": "#"}],
  "system": "#"
}

This is a connectivity/auth check using the cheapest model. The proxy must handle this and return a valid response for Claude Code to proceed.

Expected response: Standard Messages API response (non-streaming).

8. Profile

GET https://api.anthropic.com/api/oauth/profile
Authorization: Bearer {access_token}

Response:

{
  "account": {
    "uuid": "00000000-...",
    "full_name": "Jane",
    "display_name": "Jane",
    "email": "user@example.invalid",
    "has_claude_max": true,
    "has_claude_pro": false,
    "created_at": "2026-01-01T01:01:01.012345Z"
  },
  "organization": {
    "uuid": "00000000-...",
    "name": "Organization Name",
    "organization_type": "claude_max",
    "billing_type": "stripe_subscription",
    "rate_limit_tier": "default_claude_max_20x",
    "has_extra_usage_enabled": true,
    "subscription_status": "active",
    "subscription_created_at": "2026-01-01T01:01:01.012345Z"
  },
  "application": {
    "uuid": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
    "name": "Claude Code",
    "slug": "claude-code"
  }
}

9. Roles

GET https://api.anthropic.com/api/oauth/claude_cli/roles
Authorization: Bearer {access_token}
{
  "organization_uuid": "000000-...",
  "organization_name": "Organization Name",
  "organization_role": "admin",
  "workspace_uuid": null,
  "workspace_name": null,
  "workspace_role": null
}

10. Usage

GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer {access_token}
{
  "five_hour": {
    "utilization": 5.0,
    "resets_at": "2026-01-01T01:01:01.012345+00:00"
  },
  "seven_day": {
    "utilization": 4.0,
    "resets_at": "2026-01-01T01:01:01.012345+00:00"
  },
  "seven_day_oauth_apps": null,
  "seven_day_opus": null,
  "seven_day_sonnet": {
    "utilization": 1.0,
    "resets_at": "2026-01-01T01:01:01.012345+00:00"
  },
  "seven_day_cowork": null,
  "iguana_necktie": null,
  "extra_usage": {
    "is_enabled": true,
    "monthly_limit": 100,
    "used_credits": 0.0,
    "utilization": null
  }
}

Messages API (Inference)

Request

POST https://api.anthropic.com/v1/messages?beta=true

Required Headers:

accept: application/json
authorization: Bearer {access_token}
content-type: application/json
user-agent: claude-cli/2.1.77 (external, cli)
anthropic-beta: claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24
anthropic-dangerous-direct-browser-access: true
anthropic-version: 2023-06-01
x-app: cli

Note: The anthropic-beta flags vary per request. The CLI uses different combinations depending on context (preflight, inference, thinking mode). The proxy should forward whatever the CLI sends, not hardcode a specific set. The only critical flag is oauth-2025-04-20 — without it, OAuth tokens are rejected entirely.

Observed anthropic-beta variants (v2.1.77):

Context Flags
Inference (Opus, adaptive thinking) claude-code-20250219,oauth-2025-04-20,adaptive-thinking-2026-01-28,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24
Inference (1M context) claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24
Preflight (Haiku check) oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05
MCP server listing mcp-servers-2025-12-04
Init endpoints (profile, settings, etc.) oauth-2025-04-20

Stainless SDK Headers (informational, sent by the Anthropic JS SDK):

x-stainless-arch: arm64
x-stainless-lang: js
x-stainless-os: MacOS
x-stainless-package-version: 0.74.0
x-stainless-retry-count: 0
x-stainless-runtime: node
x-stainless-runtime-version: v24.3.0
x-stainless-timeout: 600

Request Body Structure:

{
  "model": "claude-opus-4-6",
  "max_tokens": 32000,
  "stream": true,
  "thinking": {
    "type": "adaptive"
  },
  "system": [
    {
      "type": "text",
      "text": "x-anthropic-billing-header: cc_version=2.1.77.<hash>; cc_entrypoint=cli; cch=00000;"
    },
    {
      "type": "text",
      "text": "You are Claude Code, Anthropic's official CLI for Claude.",
      "cache_control": {
        "type": "ephemeral",
        "ttl": "1h"
      }
    },
    {
      "type": "text",
      "text": "...(~23,000 chars of system instructions)...",
      "cache_control": {
        "type": "ephemeral",
        "ttl": "1h"
      }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "..."
    },
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "..."
        },
        {
          "type": "text",
          "text": "...(user's actual message)...",
          "cache_control": {
            "type": "ephemeral",
            "ttl": "1h"
          }
        }
      ]
    }
  ],
  "tools": [
    {
      "name": "Agent",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Bash",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Glob",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Grep",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Read",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Edit",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Write",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "Skill",
      "description": "...",
      "input_schema": { ... }
    },
    {
      "name": "ToolSearch",
      "description": "...",
      "input_schema": { ... }
    }
  ],
  "metadata": {
    "user_id": "user_{device_hash}_account_{account_uuid}_session_{session_uuid}"
  },
  "context_management": {
    "edits": [
      {
        "type": "clear_thinking_20251015",
        "keep": "all"
      }
    ]
  },
  "output_config": {
    "effort": "medium"
  }
}

Key observations:

  • stream: true — responses are always SSE streamed
  • thinking.type: "adaptive" — extended thinking is enabled
  • System prompt block 1 is a billing header (x-anthropic-billing-header) — no cache_control
  • System prompt blocks 2 and 3 use cache_control with ephemeral type and 1h TTL
  • The last content block in messages also uses cache_control for prompt caching
  • tools array contains 9 tool definitions (Agent, Bash, Glob, Grep, Read, Edit, Write, Skill, ToolSearch) — these are sent on every request
  • metadata.user_id follows the format: user_{device_hash}_account_{account_uuid}_session_{session_uuid}
  • context_management.edits controls thinking context behaviour
  • output_config.effort can be "low", "medium", or "high"
  • Total request body can be 70-120KB+ depending on conversation length

Response

Content-Type: text/event-stream; charset=utf-8

See SSE Streaming Response Format below.


SSE Streaming Response Format

Responses use Server-Sent Events format:

event: message_start
data: {"type":"message_start","message":{"model":"claude-sonnet-4-6","id":"msg_1234567890ABCDEFGHIJKLMN","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100,"cache_read_input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":100},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: ping
data: {"type": "ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"OK"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":100,"cache_read_input_tokens":100,"output_tokens":100},"context_management":{"applied_edits":[]}}

event: message_stop
data: {"type":"message_stop"}

Event sequence:

  1. message_start — contains model, message ID, initial usage with cache stats
  2. content_block_start — starts a content block (text, thinking, tool_use)
  3. ping — keepalive (sent periodically during long responses)
  4. content_block_delta — incremental content (text_delta, thinking_delta, input_json_delta)
  5. content_block_stop — ends a content block
  6. message_delta — final stop_reason, cumulative usage, and context_management
  7. message_stop — end of response

Usage fields in message_start:

{
  "input_tokens": 3,
  "cache_creation_input_tokens": 100,
  "cache_read_input_tokens": 100,
  "cache_creation": {
    "ephemeral_5m_input_tokens": 0,
    "ephemeral_1h_input_tokens": 100
  },
  "output_tokens": 1,
  "service_tier": "standard",
  "inference_geo": "not_available"
}

Important:

  • Each data: line may have trailing whitespace/padding — the proxy must preserve this
  • The context_management field appears in message_delta with applied_edits array
  • The proxy must stream SSE events in real-time — do not buffer the entire response

Rate Limit Headers

The Messages API response includes these rate limit headers:

anthropic-organization-id: {org_uuid}
anthropic-ratelimit-unified-status: allowed
anthropic-ratelimit-unified-reset: 1773723880
anthropic-ratelimit-unified-5h-status: allowed
anthropic-ratelimit-unified-5h-reset: 1773723880
anthropic-ratelimit-unified-5h-utilization: 0.01
anthropic-ratelimit-unified-7d-status: allowed
anthropic-ratelimit-unified-7d-reset: 1773723880
anthropic-ratelimit-unified-7d-utilization: 0.01
anthropic-ratelimit-unified-7d_sonnet-status: allowed
anthropic-ratelimit-unified-7d_sonnet-reset: 1773723880
anthropic-ratelimit-unified-7d_sonnet-utilization: 0.01
anthropic-ratelimit-unified-representative-claim: five_hour
anthropic-ratelimit-unified-fallback-percentage: 0.5
anthropic-ratelimit-unified-overage-disabled-reason: out_of_credits
anthropic-ratelimit-unified-overage-status: rejected

Notes:

  • The 7d_sonnet variant appears when using Sonnet models
  • representative-claim indicates which rate limit window is most relevant
  • overage-status: rejected with overage-disabled-reason: out_of_credits means extra usage credits are exhausted
  • The proxy must forward all anthropic-ratelimit-* headers — Claude Code uses them for the /usage display and rate limit decisions

Additional response headers to forward:

request-id: req_1234567890ABCDEFGHIJKLMN
anthropic-organization-id: 00000000-...
server-timing: proxy;dur=100
x-envoy-upstream-service-time: 100
content-security-policy: default-src 'none'; frame-ancestors 'none'
x-robots-tag: none

Authentication Patterns

OAuth Token (Authorization: Bearer)

Used for all API calls after authentication:

Authorization: Bearer sk-ant-oat01-...

Endpoints using Bearer auth:

  • All /api/oauth/* endpoints
  • /api/eval/sdk-*
  • /api/claude_code_grove
  • /api/claude_code_penguin_mode
  • /v1/messages
  • /v1/mcp_servers
  • /api/event_logging/v2/batch
  • /api/event_logging/batch
  • /api/claude_code/metrics
  • All mcp-proxy.anthropic.com requests

No Auth

  • POST /v1/oauth/token on platform.claude.com (uses code + PKCE instead)
  • GET /claude-code-releases/latest on storage.googleapis.com (public version check)

MCP Server Proxying

Claude Code connects to remote MCP servers through Anthropic's proxy:

POST https://mcp-proxy.anthropic.com/v1/mcp/{server_id}
Authorization: Bearer {access_token}
Content-Type: application/json
Accept: application/json, text/event-stream
User-Agent: claude-code/2.1.77 (cli)
X-MCP-Client-Session-Id: {uuid}

Initialize request:

{
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "roots": {},
      "elicitation": {
        "form": {},
        "url": {}
      }
    },
    "clientInfo": {
      "name": "claude-code",
      "version": "2.1.77"
    }
  },
  "jsonrpc": "2.0",
  "id": 0
}

Note: MCP servers may require separate OAuth tokens (not the Claude Code OAuth token). If the server requires auth, the response will be:

{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "MCP server requires authentication but no OAuth token is configured."
  },
  "request_id": "req_..."
}

Status code: 401


Telemetry & Non-Essential Endpoints

These endpoints can be stubbed with success responses if not needed:

Event Logging (v2 — current)

POST https://api.anthropic.com/api/event_logging/v2/batch
Authorization: Bearer {access_token}
Content-Type: application/json

Stub response:

{"accepted_count": 0, "rejected_count": 0}

Event Logging (v1 — legacy, still used)

POST https://api.anthropic.com/api/event_logging/batch
Authorization: Bearer {access_token}
Content-Type: application/json

Stub response:

{"accepted_count": 0, "rejected_count": 0}

Metrics

POST https://api.anthropic.com/api/claude_code/metrics
Authorization: Bearer {access_token}

Can return: 200 OK with empty body.

Datadog Logs (External — Direct)

POST https://http-intake.logs.us5.datadoghq.com/api/v2/logs

Not proxied through Anthropic — goes direct to Datadog. Can be blocked entirely.

Segment Analytics (External — Direct)

POST https://api.segment.io/v1/batch

Not proxied through Anthropic — goes direct to Segment. Can be blocked entirely.


Version Check

GET https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest

Response: Plain text version number (e.g., 2.1.71)

No authentication required. Can be stubbed or proxied. Called periodically (multiple times per session).


Full Request Body Reference

System Prompt Structure

The system prompt is sent as an array of 3 text blocks:

Block Content Cache Control Typical Size
0 Billing header (REQUIRED for Sonnet/Opus — see Model Access) None ~80 chars
1 Identity: You are Claude Code, Anthropic's official CLI for Claude. ephemeral, 1h ~57 chars
2 Full system instructions (tools usage, safety rules, tone, etc.) ephemeral, 1h ~23,000 chars

CRITICAL: Block 0 — Billing Header

The first system block must be the billing header. Without it, Sonnet and Opus models return 400 Error (Haiku works without it):

x-anthropic-billing-header: cc_version=2.1.77.b88; cc_entrypoint=cli; cch=00000;
Field Value Purpose
cc_version 2.1.77.<hash> CLI version + 3-char content hash (see algorithm below)
cc_entrypoint cli Entry point (cli, vscode, sdk, etc.) or CLAUDE_CODE_ENTRYPOINT env var
cch 00000 Static placeholder (was a computed checksum in earlier versions)
cc_workload (optional) Workload identifier if configured

Billing header hash algorithm (v2.1.77):

The 3-char hash appended to cc_version is derived from the first user message in the conversation:

const salt = "59cf53e54c78";
const VERSION = "2.1.77";

// 1. Extract text of the first "user" type message from the normalized messages array
const text = messages.find(m => m.type === "user")?.message.content; // get text

// 2. Take characters at 0-indexed positions 4, 7, 20 (default "0" if missing)
const chars = [4, 7, 20].map(i => text[i] || "0").join("");

// 3. SHA-256 hash the concatenation, take first 3 hex chars
const hash = crypto.createHash("sha256")
    .update(salt + chars + VERSION)
    .digest("hex")
    .slice(0, 3);

// 4. cc_version becomes "2.1.77.<hash>" e.g. "2.1.77.b88"
// 5. cch is always "00000" (static)

Example: For first user message "Hello, how are you?":

  • Chars at [4,7,20] = "o", "h", "0" (pos 20 doesn't exist) → "oh0"
  • Input: "59cf53e54c78oh02.1.77"
  • SHA-256 → first 3 hex chars: "b88"
  • Header: cc_version=2.1.77.b88; cc_entrypoint=cli; cch=00000;

Feature flag: The billing header is controlled by the tengu_attribution_header feature flag and can be disabled via CLAUDE_CODE_ATTRIBUTION_HEADER=false env var.

This block must have no cache_control — it is not cached. The proxy must preserve this block exactly as sent by the CLI.

Tools Sent Per Request

9 tools are defined in every messages request:

Tool Purpose
Agent Launch sub-agents for complex tasks
Bash Execute shell commands
Glob File pattern matching
Grep Content search (ripgrep-based)
Read Read file contents
Edit Edit files with string replacement
Write Write/create files
Skill Invoke slash command skills
ToolSearch Discover deferred tools

Metadata Structure

{
  "user_id": "user_{device_hash}_account_{account_uuid}_session_{session_uuid}"
}

Context Management

{
  "edits": [
    {
      "type": "clear_thinking_20251015",
      "keep": "all"
    }
  ]
}

Output Config

{
  "effort": "medium"
}

Valid effort levels: "low", "medium", "high"


Proxy Implementation Checklist

A complete proxy must:

  • Handle OAuth authorization redirect (claude.ai/oauth/authorize)
  • Handle OAuth token exchange (platform.claude.com/v1/oauth/token)
  • Handle token refresh (same endpoint, grant_type=refresh_token)
  • Forward Authorization: Bearer tokens on all API requests
  • Support SSE streaming for /v1/messages responses (do NOT buffer)
  • Forward all anthropic-ratelimit-* headers from Messages API responses
  • Forward the anthropic-beta header (required for beta features)
  • Forward anthropic-dangerous-direct-browser-access: true
  • Forward anthropic-version: 2023-06-01
  • Forward request-id and anthropic-organization-id response headers
  • Handle the preflight Haiku messages request (connectivity check)
  • Handle /api/oauth/profile — returns account/org/subscription info
  • Handle /api/oauth/claude_cli/roles — returns org role
  • Handle /api/oauth/account/settings — returns feature flags
  • Handle /api/oauth/usage — returns rate limit utilization
  • Handle /api/oauth/claude_cli/client_data — returns client config
  • Handle /api/claude_code_grove — returns grove status
  • Handle /api/claude_code_penguin_mode — returns penguin mode status
  • Handle /api/eval/sdk-{hash} — returns GrowthBook feature flags
  • Handle /v1/mcp_servers listing
  • Optionally proxy MCP server requests (mcp-proxy.anthropic.com)
  • Handle or stub telemetry endpoints (event_logging/v2/batch, event_logging/batch, metrics)
  • Preserve cache_control in request bodies (affects billing/performance)
  • Support ?beta=true query parameter on messages endpoint
  • Handle large request bodies (70-120KB+ for messages with tools + system prompt)

Environment Variables for Claude Code

To point Claude Code at your proxy:

# Route API calls through your proxy (only /v1/messages and /v1/mcp_servers)
ANTHROPIC_BASE_URL=https://your-proxy.example.com claude

# Route ALL traffic (including OAuth, profile, feature flags) through a forward proxy
HTTPS_PROXY=https://your-proxy.example.com claude

# If your proxy uses a custom CA certificate
NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem claude

# Combine for full proxy with custom CA
HTTPS_PROXY=https://your-proxy.example.com \
NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem \
claude

Note: ANTHROPIC_BASE_URL only affects the Messages API endpoint. OAuth, profile, feature flags, and telemetry still go directly to api.anthropic.com and platform.claude.com. Use HTTPS_PROXY for full traffic interception.


Important: Model Access & Token Behaviour

Two gates for Sonnet/Opus access

Both of these are required for Sonnet and Opus models to work with OAuth tokens:

  1. anthropic-beta header must include oauth-2025-04-20 — without it, ALL OAuth requests fail with 401 "OAuth authentication is currently not supported"
  2. Billing header must be present in system prompt block 0 — without it, Sonnet/Opus return 400 "Error" (Haiku works without it)

Required anthropic-beta flags (v2.1.77):

anthropic-beta: claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24

Billing header is required for Sonnet/Opus

The billing header in system prompt block 0 gates access to Sonnet and Opus models. The proxy must either preserve the billing header from the CLI or generate it.

Billing header format:

x-anthropic-billing-header: cc_version=2.1.77.<hash>; cc_entrypoint=cli; cch=00000;

See Billing Header Hash Algorithm for the hash computation.

Token invalidation

OAuth access tokens can be invalidated by certain API errors. Specifically:

  • 401 auth errors (wrong/missing oauth-2025-04-20 beta flag) burn the token — all subsequent requests return 401 "Invalid authentication credentials"
  • 400 errors (e.g., missing billing header for Opus) do NOT invalidate the token — the same token works on the next correct request

Implications for proxy implementations:

  • The proxy must not strip or modify the anthropic-beta header — a 401 will burn the client's token
  • The proxy must preserve the billing header system block — without it, Sonnet/Opus return 400
  • A 400 from a missing billing header is recoverable (same token works on retry), but a 401 from wrong beta flags is not
  • If the proxy needs to generate the billing header, it must compute the cc_version hash correctly (see algorithm above)

Verified access matrix

Scenario Haiku Sonnet Opus
OAuth + beta flag + billing header 200 200 200
OAuth + beta flag + NO billing header 200 400 400
OAuth without oauth-2025-04-20 flag 401 (burns token) 401 (burns token) 401 (burns token)

Implications for proxy implementations

  • The proxy must forward the anthropic-beta header exactly as sent by the CLI
  • The proxy must preserve the billing header in system prompt block 0 (or generate it)
  • The proxy should forward all request headers without modification
  • If the proxy needs to inject its own system prompt content, add it as additional blocks after block 0
  • Token exchange and refresh can be proxied transparently
  • 400 errors are safe (token survives), 401 errors are destructive (token is burned)

Verified working endpoints with OAuth tokens:

  • GET /api/oauth/profile — works
  • GET /api/oauth/claude_cli/roles — works
  • GET /api/oauth/account/settings — works
  • GET /api/oauth/usage — works (requires oauth-2025-04-20 beta context)
  • POST /v1/messages with Haiku — works without billing header
  • POST /v1/messages with Sonnet/Opus — works only with billing header
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment