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.
pip install anthropicnpm install @anthropic-ai/sdkSet 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.
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)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.
count = client.messages.count_tokens(
model="<model-id>",
messages=[{"role": "user", "content": "Hello"}],
)
print(count.input_tokens)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).
client = Anthropic(
base_url="https://api.noirdoc.de",
auth_token=os.environ["NOIRDOC_API_KEY"],
default_headers={"X-Noirdoc-Shield": "on"},
)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.
export ANTHROPIC_BASE_URL="https://api.noirdoc.de"
export ANTHROPIC_AUTH_TOKEN="$NOIRDOC_API_KEY"
export ANTHROPIC_MODEL="<model-id>"
claudeHandle 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.
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)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. |