Develop / Guides

Anthropic SDK & Claude Code

How to point the Anthropic SDK and Claude Code at the gateway, count tokens and handle errors from /v1/messages.

The gateway accepts requests in the format of the Anthropic API at POST /v1/messages, plus POST /v1/messages/count_tokens. The Anthropic SDK and Claude Code therefore work as soon as the base URL and the key point to Noirdoc.

Install the SDK

Install the anthropic package for Python or @anthropic-ai/sdk for Node.js.

Shell
pip install anthropic
Shell
npm install @anthropic-ai/sdk

Set up the client

Set the base URL to https://api.noirdoc.de, without /v1. The SDK appends the path /v1/messages itself.

Pass the px- key as auth_token (TypeScript: authToken). The SDK then sends it in the Authorization: Bearer header. Passed as api_key, the SDK sends it in the x-api-key header. The gateway accepts both headers.

The SDK also reads the values from ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY. Set one of the two key variables, not both. Otherwise the SDK sends both headers.

Python
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.noirdoc.de",
    auth_token=os.environ["NOIRDOC_API_KEY"],
)

message = client.messages.create(
    model="<model-id>",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello"}],
)
print(message.content[0].text)
TypeScript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.noirdoc.de",
  authToken: process.env.NOIRDOC_API_KEY,
});

const message = await client.messages.create({
  model: "<model-id>",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello" }],
});
console.log(message.content);

Count tokens

client.messages.count_tokens (TypeScript: countTokens) calls POST /v1/messages/count_tokens through the gateway. The method takes model and messages like messages.create and returns the number of input tokens. The endpoint skips Noirdoc Shield at €0.

Python
count = client.messages.count_tokens(
    model="<model-id>",
    messages=[{"role": "user", "content": "Hello"}],
)
print(count.input_tokens)
TypeScript
const count = await client.messages.countTokens({
  model: "<model-id>",
  messages: [{ role: "user", content: "Hello" }],
});
console.log(count.input_tokens);

Turn on Noirdoc Shield

Send the X-Noirdoc-Shield: on header (X-Noirdoc-Mask remains supported as a compatible alias), with default_headers (TypeScript: defaultHeaders) for every request of the client. Noirdoc Shield statelessly redacts personal data such as [PERSON] or [IBAN] before forwarding and blocks detected secrets ([SECRET]) and prompt injections with status code 403 (shield_violation). Responses are not reverse-translated. The gateway removes the header before forwarding the request to the provider. Whether the header takes effect depends on the settings of your organisation, API key and any attached request tags (see Set up Noirdoc Shield).

Python
client = Anthropic(
    base_url="https://api.noirdoc.de",
    auth_token=os.environ["NOIRDOC_API_KEY"],
    default_headers={"X-Noirdoc-Shield": "on"},
)
TypeScript
const client = new Anthropic({
  baseURL: "https://api.noirdoc.de",
  authToken: process.env.NOIRDOC_API_KEY,
  defaultHeaders: { "X-Noirdoc-Shield": "on" },
});

Set up Claude Code

Claude Code reads the environment variables ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY. Set ANTHROPIC_BASE_URL to https://api.noirdoc.de and ANTHROPIC_AUTH_TOKEN to your px- key. Claude Code then sends the key in the Authorization: Bearer header.

ANTHROPIC_API_KEY works as well and sends the key in the x-api-key header. Do not set both variables.

You choose the model with ANTHROPIC_MODEL, for example with a model ID from GET /v1/models. Claude Code is built for Claude models; Anthropic does not support Claude Code with other models through a gateway.

Claude Code sets additional headers from ANTHROPIC_CUSTOM_HEADERS, one Name: Value pair per line. This is how you turn on Noirdoc Shield for Claude Code: ANTHROPIC_CUSTOM_HEADERS="X-Noirdoc-Shield: on" (or set shield_mode to On directly on the API key).

Claude Code preflight-counts the tokens in the context with POST /v1/messages/count_tokens. This endpoint skips Noirdoc Shield at €0, while actual POST /v1/messages calls run through Shield when enabled. For Claude through Google Vertex, the gateway forwards the counting request to the Vertex counting method; details are under Messages.

Shell
export ANTHROPIC_BASE_URL="https://api.noirdoc.de"
export ANTHROPIC_AUTH_TOKEN="$NOIRDOC_API_KEY"
export ANTHROPIC_MODEL="<model-id>"
claude

Handle errors

Every error response from the gateway has an error object with type, code and message, plus the request ID in request_id. The path determines the format: /v1/messages and /v1/skills always answer in the Anthropic format, all other endpoints in the OpenAI format. Only /v1/files follows the header: with x-api-key it answers in the Anthropic format, with auth_token (Bearer) in the OpenAI format. The gateway reports provider errors in the uniform error object: capacity problems as 429 resource_exhausted and overload as 529 provider_overloaded, each with Retry-After, rejected requests as provider_rejected_request with the provider’s message, and all other errors as 502 provider_error. On routes with your own provider key (BYOK), the provider’s status codes 401, 402, 403 and 429 arrive as provider_rejected_request with the provider’s status code and message, as described under Error codes. A missing or invalid key results in status code 401 with the code invalid_api_key. The SDK retries requests with the status codes 408, 409, 429 and 5xx until max_retries is reached. It waits for the time given in Retry-After if the header states 60 seconds or less; otherwise it increases the wait exponentially.

The type follows the status code, for example rate_limit_error for 429. The Anthropic SDKs do not expose type and code directly on the error. You read both from the body: in Python from e.body["error"]["type"] and e.body["error"]["code"], in TypeScript from err.error?.error?.type and err.error?.error?.code. In your code, check the code, not the message.

Python
import anthropic

try:
    message = client.messages.create(
        model="<model-id>",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello"}],
    )
except anthropic.APIStatusError as e:
    code = e.body["error"]["code"]
    if code == "provider_rejected_request":
        provider = e.response.headers.get("x-noirdoc-provider")
        msg = e.body["error"]["message"]
        print("Provider rejected:", provider, msg)
    else:
        print("Error:", e.status_code, code)
TypeScript
type GatewayBody = {
  error?: { type?: string; code?: string; message?: string };
};

try {
  const message = await client.messages.create({
    model: "<model-id>",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello" }],
  });
} catch (err) {
  if (!(err instanceof Anthropic.APIError)) throw err;
  const body = err.error as GatewayBody | undefined;
  if (body?.error?.code === "provider_rejected_request") {
    const provider = err.headers?.get("x-noirdoc-provider");
    const msg = body.error.message;
    console.log("Provider rejected:", provider, msg);
  } else {
    console.log("Error:", err.status, body?.error?.code);
  }
}

All codes and how to fix them are listed under Error codes. Streaming describes streaming with messages.stream.

Status Code Meaning
400 wrong_endpoint_for_model The model belongs to a different endpoint family or API format than the endpoint called.
402 insufficient_credit The organisation's credit is used up or lower than the estimated maximum cost of the request (including Noirdoc Shield, also on BYOK routes when Shield is active).
402 key_budget_exhausted This key's budget is used up for the current period or does not cover the estimated cost of the request.
403 model_not_allowed_for_key The model is not one of this key's allowed models.
403 shield_violation Noirdoc Shield blocked the request before forwarding to the provider because an active protection filter configured with block matched; message names the triggered rule (secrets, pii, ner, injection or custom).
404 model_not_available The model ID is not available to the organisation.