Point the Anthropic Python SDK at a mock server with ANTHROPIC_BASE_URL

M
Matthew Diakonov
8 min read

Direct answer (verified 2026-06-17)

Set ANTHROPIC_BASE_URL=http://localhost:8083 (or pass base_url= to Anthropic()). The SDK reads os.environ.get("ANTHROPIC_BASE_URL") at construction and routes every request to that root instead of api.anthropic.com. You must still set a non-empty ANTHROPIC_API_KEY or the client raises before it ever reaches your mock.

Source of record: anthropics/anthropic-sdk-python.

Every other guide on this stops at the first line: the Anthropic Python SDK reads ANTHROPIC_BASE_URL, so export it and your requests go to your mock. That is true, and it is the easy 80%. The part nobody writes down is the two ways the seam fails silently, and both of them cost an afternoon if you have not seen them before.

I learned them the hard way shipping this exact mechanism in production. Fazm is an open-source macOS app that wraps Claude Code, and it lets you point the underlying agent at any Anthropic-compatible endpoint. Under the hood that is the same ANTHROPIC_BASE_URL override you use for a mock server in tests. The code that decides whether a given value is safe to forward is the most useful thing on this page, so it is below verbatim, with line numbers.

What the override actually changes

Setting the variable does not intercept anything magically. It only changes the root the SDK prepends to every path. The auth header, the request body, the retry logic, all of it is unchanged. Your mock just stands where the real host used to.

Where ANTHROPIC_BASE_URL sits in a request

Your codeAnthropic() clientMock servermessages.create(...)base_url = ANTHROPIC_BASE_URL or defaultPOST {base_url}/v1/messagesspec-shaped JSON (or SSE stream)Message object

The minimal setup, end to end

1

Start a mock that speaks the Messages API

The SDK's own tests use Prism against the Anthropic OpenAPI spec, which gives you schema-accurate responses for free. Any HTTP server returning the JSON shapes your code reads also works.

# Prism against the public OpenAPI spec
npx @stoplight/prism-cli mock anthropic-openapi.yml
# -> listening on http://127.0.0.1:4010
2

Point the SDK at it

Either export the environment variable before the process starts, or pass base_url= explicitly. The constructor arg wins when both are set.

import anthropic

client = anthropic.Anthropic(
    base_url="http://127.0.0.1:4010",
    api_key="sk-test",   # required, value is ignored by the mock
)
print(client.base_url)   # confirm what the SDK parsed
3

Or do it with zero code change via the environment

Useful in CI where you run the same code against the mock. Use echo -n so no trailing newline corrupts the host.

export ANTHROPIC_BASE_URL=http://127.0.0.1:4010
export ANTHROPIC_API_KEY=sk-test
python run_my_code.py
4

Send a request and assert against the mock

The call goes to your mock, not to api.anthropic.com. Assert on the captured request body, or on the spec-shaped response Prism returns.

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=16,
    messages=[{"role": "user", "content": "ping"}],
)
# No network call left your machine.

Gotcha 1: a malformed base URL bricks the client, often silently

If the value is not a complete absolute URL with a scheme and a host, you do not get a clear error at startup. You get one of two worse outcomes: the SDK treats your value as a path against the default host, so requests quietly still hit production; or it raises Invalid URL on the first call, deep inside the request path where it is easy to mistake for a network problem.

The values that bite people: a bare localhost:8083 with no scheme, a value with a stray trailing newline (you piped it through echo instead of echo -n), or a host that resolves but has no listener. The defensive move is to validate the value before you hand it to the SDK.

Here is the exact gate Fazm runs before writing anything into ANTHROPIC_BASE_URL. It lives in Desktop/Sources/Chat/ACPBridge.swift at line 301. Swift, but the rule ports to one line of Python: reject anything that is not scheme + host.

static func validCustomAPIEndpoint(_ raw: String) -> String? {
  let endpoint = raw.trimmingCharacters(in: .whitespacesAndNewlines)
  guard !endpoint.isEmpty,
    let url = URL(string: endpoint),
    let scheme = url.scheme?.lowercased(),
    scheme == "http" || scheme == "https",
    let host = url.host,
    !host.isEmpty else {
    return nil          // fall back to the default endpoint
  }
  return endpoint
}

The comment above the call site spells out the failure mode the way only production scar tissue does: a malformed value otherwise lands in ANTHROPIC_BASE_URL and makes the Anthropic SDK throw "API Error: Invalid URL" on every query, silently bricking built-in chat. The Python equivalent:

from urllib.parse import urlparse

def valid_base_url(raw: str) -> str | None:
    raw = raw.strip()
    p = urlparse(raw)
    if p.scheme in ("http", "https") and p.netloc:
        return raw
    return None  # do not pass this to the SDK

Gotcha 2: the API key is still required, and it can leak to your mock

The SDK constructor validates that a key exists before it makes any request. Point it at a mock with no ANTHROPIC_API_KEY set and you get an AnthropicError at construction time, before your mock is ever contacted. So a dummy key is not optional, it is what lets the client get far enough to call your server at all.

There is a second, sharper edge once you go past tests into a real gateway: whatever key is in the environment gets sent in the x-api-key header to whatever host ANTHROPIC_BASE_URL points at. If that host is a third-party proxy or a teammate's mock, you have just handed your real Anthropic key to it. Fazm handles this by overwriting the bundled key with a harmless placeholder the moment a custom endpoint is set (ACPBridge.swift, lines 2426 to 2433):

if let customEndpoint = Self.validCustomAPIEndpoint(rawCustomEndpoint) {
  env["ANTHROPIC_BASE_URL"] = customEndpoint
  env["FAZM_CUSTOM_API_ENDPOINT"] = "true"
  // Never send the bundled Anthropic key to a custom endpoint.
  // A placeholder keeps Anthropic-compatible gateways on the
  // API-key path instead of triggering Claude OAuth.
  env["ANTHROPIC_API_KEY"] = "sk-fazm-custom-endpoint"
}

The lesson generalizes: when the base URL is not the real Anthropic API, the key in the environment should be a throwaway, not your live credential. In tests this is automatic because you use sk-test. In any setup where the base URL is user-configurable, scrub the real key the same way Fazm does.

Pre-flight checklist before you blame the mock

  • ANTHROPIC_BASE_URL is a full http(s) URL with a host, no bare host:port
  • No trailing newline in the value (set it with echo -n, not echo)
  • ANTHROPIC_API_KEY is set to any non-empty string, even for a mock
  • The real key is a throwaway when the base URL is not api.anthropic.com
  • Printed client.base_url to confirm what the SDK actually parsed
  • Mock returns text/event-stream for any code path that calls messages.stream()

Routing an agent through a custom endpoint, not just a test?

If you are wiring Claude Code or a Claude agent through a proxy or gateway and hitting the base-URL and key edges, I am happy to compare notes on a quick call.

Frequently asked questions

Does the Anthropic Python SDK read ANTHROPIC_BASE_URL automatically?

Yes. When you construct `anthropic.Anthropic()` with no explicit `base_url`, the client resolves the base URL from `os.environ.get("ANTHROPIC_BASE_URL")` and falls back to the default api.anthropic.com host only if that variable is unset or empty. So you can point every request at a mock by exporting `ANTHROPIC_BASE_URL=http://localhost:8083` before the process starts, with no code change. Passing `base_url=` to the constructor does the same thing and wins over the environment variable when both are present.

Do I still need an API key if the real Anthropic API is never contacted?

Yes, the client constructor still needs one. `anthropic.Anthropic()` raises an `AnthropicError` at construction time if it cannot find a key in `api_key=` or `ANTHROPIC_API_KEY`, and that check runs before any HTTP request is made, so it fails before your mock ever sees a connection. Set `ANTHROPIC_API_KEY=sk-test` (any non-empty string) when pointing at a mock or a gateway that does its own auth. Your mock can ignore the value entirely.

Why does my mock server never receive the request after I set the base URL?

The most common cause is a malformed value. `ANTHROPIC_BASE_URL` must be a full absolute URL with a scheme and host, like `http://localhost:8083`. A bare `localhost:8083`, a value missing `http://`, or stray whitespace either gets treated as a relative path against the default host or makes the client raise an Invalid URL error on the first call. The second cause is a trailing-newline in the variable (common when you pipe a value via `echo` instead of `echo -n`), which corrupts the host. Print the resolved `client.base_url` to confirm what the SDK actually parsed.

What mock server do the SDK's own tests use?

The anthropic-sdk-python repository runs its test suite against a Prism mock server (`npx prism mock path/to/openapi.yml`) started on a local port, then points the client at it. Prism validates each incoming request against the Anthropic OpenAPI spec and returns spec-shaped responses, which is what lets the tests exercise error states and edge cases deterministically without hitting production. You do not have to use Prism; any HTTP server that returns the JSON shapes your code reads will work, but Prism gets you schema-accurate responses for free.

How do I mock streaming responses, not just a single JSON body?

Your mock has to return a `text/event-stream` response with the same Server-Sent Events the real API emits: `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`. The SDK's streaming parser reads those event names off the wire, so a plain JSON body will not satisfy `client.messages.stream()`. If you only need request assertions and not realistic token-by-token output, mock the non-streaming `messages.create` path instead and keep streaming tests behind a separate fixture.

Is ANTHROPIC_BASE_URL only for tests, or is it used in production too?

Both. The same variable that points the SDK at a mock in a test is the seam production apps use to route through a corporate proxy, an Anthropic-compatible gateway, or a self-hosted relay. Fazm, an open-source macOS app that wraps Claude Code, exposes a Custom API Endpoint setting that does exactly this: it writes the validated value into `ANTHROPIC_BASE_URL` for the underlying agent process. The test path and the production path are the same code path, which is why the validation and key-handling gotchas below matter in both.

Can I set ANTHROPIC_BASE_URL per request instead of globally?

Not via the environment variable, which is read once at client construction. For per-request or per-test routing, construct distinct clients: `Anthropic(base_url="http://localhost:8083")` for the mock and a default `Anthropic()` for the real API, and inject whichever one the code under test should use. This is cleaner than mutating `os.environ` between tests, which is process-global and leaks across test cases if a client was already constructed.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.