How to Get a Claude API Key: Test the Gateway Credential First
A safe setup guide that distinguishes an Anthropic-issued key from a Crazyrouter gateway key. Two live Claude Sonnet requests and six local destination checks show what was actually verified.

A Claude API key issued by Anthropic belongs to an Anthropic account and native endpoint. A Crazyrouter key is a different credential for a gateway that can route Claude models. This article tests the latter; it cannot create or verify a native Anthropic key.
Crazyrouter was tested with two Claude Sonnet 4.6 gateway protocols; both produced the exact marker, while six local credential-destination cases passed.
By Jeff M · Tested November 4, 2026
How do I get a Claude API key and check that it works?#
Create a vendor-native key in the vendor console if you need direct vendor billing and its native account controls. For this tested gateway path, obtain a Crazyrouter key through your own account and keep it in a process environment variable. Never paste a key into a blog, terminal recording or browser query string. The code validates the configured destination before it sends the key. That local guard does not replace normal secret storage or rotation.
November 4 test setup and observations#
The complete Python 3.10 script ran on Windows with a gateway credential held in the process environment. The request bodies, endpoints, response IDs, terminal states and failures are reproduced below. Script exit code zero means observations were saved; it does not turn an unsuccessful model response into a pass.
| Requested exact model ID | Visible to credential |
|---|---|
| claude-sonnet-4-6 | Yes |
| Route / fixture | HTTP / error | Terminal state | Accepted | Output or error | Response ID | Usage | USD estimate |
|---|---|---|---|---|---|---|---|
| claude-sonnet-4-6 / messages | 200 | end_turn | Yes | READY | msg_011CggL5uwVtioo7eJSTGvUb | {input_tokens: 15, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, cache_creation: {ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 0}, output_tokens: 5, service_tier: standard, inference_geo: global, speed: standard} | Not estimated |
| claude-sonnet-4-6 / chat | 200 | stop | Yes | READY | msg_011CggL655AEowNrW9QYUkdx | {prompt_tokens: 15, completion_tokens: 5, total_tokens: 20, prompt_tokens_details: {cached_tokens: 0, cached_creation_tokens: 0, text_tokens: 0, audio_tokens: 0, image_tokens: 0}, completion_tokens_details: {text_tokens: 0, audio_tokens: 0, reasoning_tokens: 0}, input_tokens: 15, output_tokens: 5, claude_cache_creation_5_m_tokens: 0, claude_cache_creation_1_h_tokens: 0} | Not estimated |
| Executed local check | Result |
|---|---|
| fixed gateway accepted | Passed |
| official origin is a different credential scope | Passed |
| lookalike rejected | Passed |
| HTTP rejected | Passed |
| blank rejected | Passed |
| newline rejected | Passed |

Use the right header for each protocol#
The same gateway key was sent to https://api.crazyrouter.com/v1/messages in an x-api-key header with anthropic-version: 2023-06-01, then to /v1/chat/completions as a Bearer credential. Both Claude Sonnet 4.6 requests returned the exact word READY and completed. This proves these two gateway routes worked during the recorded test. It says nothing about authentication at api.anthropic.com, since that host was not called.
Treat a successful request as a narrow smoke test#
The six synthetic local cases reject a lookalike host, HTTP transport, an empty key, a newline-bearing value and accidental use of the official vendor host with the gateway key. The live responses have different envelope fields: native Messages uses content and stop_reason, while Chat Completions uses choices and finish_reason. Validate those fields in your application before showing a success state.
Reproduce this check#
Install requests, set CRAZYROUTER_API_KEY for an authorized account, and run this complete script with Python 3.10 or later. It may generate billable traffic. The script does not print authorization headers.
import json
import os
import time
from decimal import Decimal as D
import requests
BASE = "https://api.crazyrouter.com/v1"
def client():
s = requests.Session()
s.trust_env = False
return s
def catalog(wanted):
with client() as s:
r = s.get(BASE + "/models", headers={"Authorization": "Bearer " + os.environ["CRAZYROUTER_API_KEY"]}, timeout=(10, 40), allow_redirects=False)
r.raise_for_status()
names = {x["id"] for x in r.json()["data"]}
return {name: name in names for name in wanted}
def request(model, prompt, mode="chat", **options):
path = {"chat": "/chat/completions", "messages": "/messages", "responses": "/responses"}[mode]
payload = {"model": model, **options}
if mode == "responses":
payload["input"] = prompt
else:
payload["messages"] = [{"role": "user", "content": prompt}]
headers = {"Authorization": "Bearer " + os.environ["CRAZYROUTER_API_KEY"]}
if mode == "messages":
headers = {"x-api-key": os.environ["CRAZYROUTER_API_KEY"], "anthropic-version": "2023-06-01"}
rec = {"model": model, "endpoint": BASE + path, "request": payload}
start = time.perf_counter()
try:
with client() as s:
r = s.post(BASE + path, headers=headers, json=payload, timeout=(10, 90), allow_redirects=False)
rec["http"] = r.status_code
if r.status_code == 200:
data = r.json()
rec.update(response_id=data.get("id"), usage=data.get("usage", {}))
if mode == "messages":
output = "".join(x.get("text", "") for x in data.get("content", []) if x.get("type") == "text")
complete = data.get("stop_reason") == "end_turn"
finish = data.get("stop_reason")
elif mode == "responses":
output = "".join(c.get("text", "") for item in data.get("output", []) if item.get("type") == "message" for c in item.get("content", []) if c.get("type") == "output_text")
complete = data.get("status") == "completed"
finish = data.get("status")
else:
choice = data["choices"][0]
output = choice["message"].get("content") or ""
complete = choice.get("finish_reason") == "stop"
finish = choice.get("finish_reason")
rec.update(output=output, complete=complete, finish=finish)
else:
rec["error"] = "HTTP_" + str(r.status_code)
except (requests.RequestException, ValueError, KeyError, IndexError) as error:
rec["error"] = type(error).__name__
rec["seconds"] = round(time.perf_counter() - start, 3)
return rec
def accepted(rec, expected):
if not rec.get("complete"):
return False
try:
return json.loads(rec["output"]) == expected
except (ValueError, TypeError):
return False
def emit(**data):
print(json.dumps(data, ensure_ascii=False))
from urllib.parse import urlsplit
def validate_config(origin, value):
if origin != "https://api.crazyrouter.com/v1":
raise ValueError("Unexpected credential destination")
if not isinstance(value, str) or not value.strip() or any(c.isspace() for c in value):
raise ValueError("Invalid credential shape")
return True
tests = []
for label, origin, value, good in [
("fixed gateway accepted", BASE, "synthetic-token", True),
("official origin is a different credential scope", "https://api.anthropic.com/v1", "synthetic-token", False),
("lookalike rejected", "https://api.crazyrouter.com.example.org/v1", "synthetic-token", False),
("HTTP rejected", "http://api.crazyrouter.com/v1", "synthetic-token", False),
("blank rejected", BASE, "", False),
("newline rejected", BASE, "synthetic\ntoken", False)]:
try:
outcome = validate_config(origin, value)
except ValueError:
outcome = False
assert outcome == good
tests.append({"case": label, "passed": True})
validate_config(BASE, os.environ["CRAZYROUTER_API_KEY"])
model = "claude-sonnet-4-6"
available = catalog([model])
rows = []
if available[model]:
for mode in ["messages", "chat"]:
rec = request(model, 'Reply with exactly the word READY.', mode=mode, max_tokens=64)
rec.update(case=mode, passed=rec.get("complete", False) and rec.get("output", "").strip() == "READY")
rows.append(rec)
emit(local_tests=tests, catalog=available, requests=rows, scope="Gateway credential only; no Anthropic account created or official credential tested")

Sources and related guides#
Documentation checked November 4, 2026: Claude Messages API reference, Crazyrouter pricing configuration. The test table describes gateway observations, not vendor-native account measurements.
- One API Key for Multiple AI Models: Catalog Access and Live Route Tests
- Claude Haiku 5.5 API: Verify Messages Responses Before Adopting It
- AI API Key Safety: Build Logs That Exclude Credentials and Prompts
- OpenAI-Compatible Claude and Gemini: Normal and Streaming SDK Tests
- Prompt Caching in Python: Read Token Counters Before Claiming Savings
FAQ#
Did this create an Anthropic API key?#
No. Account creation and vendor-native billing were outside this test.
Can I use the same key at api.anthropic.com?#
Do not assume so. The tested credential was scoped to the Crazyrouter gateway.
Which gateway endpoint was tested?#
Both /v1/messages and /v1/chat/completions returned the exact marker in this run.
Should a key appear in client-side JavaScript?#
No. Keep it in a server-side secret store or environment variable.
Does one successful call prove account limits?#
No. Limits, rotation, permissions and sustained load were not tested.
What should be checked after rotating a key?#
Run a small scoped request and verify the final envelope, while avoiding key material in logs.
Last actually tested: November 4, 2026. This API-dependent article is due for review December 4, 2026.
Implementation Guides
Podcast
AI Dev Tools: The Crazyrouter Podcast
Short episodes for developers on AI APIs, gateways, cost control and coding tools.
Latest episodes
EP196 · Oct 29, 2026 · 6:53
AI API Security Posture — Turn Controls Into Continuous Evidence
A practical guide to AI API security posture: inventory exposure, verify controls continuously, protect keys and data, constrain tools, and turn security assumptions into evidence that operators can act on.
EP195 · Oct 28, 2026 · 7:21
AI API Policy Drift — Detect When Production Rules Stop Matching Intent
A practical guide to policy drift in AI APIs: version routing and safety rules, detect stale enforcement, compare intended and observed behavior, and restore control before small gaps become incidents.





