How to Test Claude API Endpoints Locally
"Testing Claude API endpoints locally" doesn't mean running Anthropic's servers on your laptop — you can't. It means building a local workflow where you can send real (or mocked) requests to the Claude API from your dev machine, inspect responses, and catch bugs before they hit staging or production. That's the workflow this article covers: environment setup, curl-based smoke tests, a minimal JS test harness, mocking for unit tests, and how to handle streaming and tool use during local development.
The good news is that Claude's API is plain HTTPS with JSON payloads, so local testing doesn't require special tooling beyond curl, a .env file, and whatever test runner your stack already uses.
Set up your local environment first
Before writing a single test, get your credentials and config sorted so you're not accidentally hitting production data or burning quota during iteration.
Use environment variables, not hardcoded keys
# .env.local
CLAUDE_API_KEY=sk-ant-xxxxxxxxxxxx
CLAUDE_MODEL=claude-sonnet-4-20250514
CLAUDE_BASE_URL=https://api.anthropic.com/v1
Load this with dotenv (Node), python-dotenv (Python), or your framework's built-in env loader. Never commit .env.local — add it to .gitignore on day one.
Keep dev and prod keys separate
If your team shares one API key across environments, a bad local test run can eat into the same rate limit and budget as production traffic. Create a dedicated key (or a separate account/project) for local and CI testing so a runaway loop in a test file doesn't affect real users.
If you're testing through SubToAPI instead of calling Anthropic directly, this is built in: each application gets its own sub_live_... key from the dashboard, so you can spin up a throwaway key for local testing and revoke it without touching your production key. See /docs/quickstart for how keys are scoped per app.
Smoke-test the endpoint with curl
Before writing any code, confirm the endpoint responds correctly with a raw curl call. This isolates network/auth issues from bugs in your application code.
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $CLAUDE_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"messages": [
{"role": "user", "content": "Reply with exactly the word: pong"}
]
}'
If this returns a valid JSON response with "pong" in the content, your credentials and network path are fine — any further bugs are in your application layer, not the API connection.
Keep a handful of these curl snippets in a scripts/ folder (test-basic.sh, test-stream.sh, test-tools.sh). They're faster to run than spinning up your whole app when you just need to check "is this endpoint even reachable."
Build a minimal JS test harness
For anything beyond a single curl call, wrap requests in a small script you can run repeatedly while iterating on prompts or parsing logic.
async function testMessage(prompt) {
const res = await fetch(`${process.env.CLAUDE_BASE_URL}/messages`, {
method: "POST",
headers: {
"x-api-key": process.env.CLAUDE_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: process.env.CLAUDE_MODEL,
max_tokens: 512,
messages: [{ role: "user", content: prompt }],
}),
});
if (!res.ok) {
console.error("Error:", res.status, await res.text());
return;
}
const data = await res.json();
console.log(data.content[0].text);
}
testMessage("Summarize this in one sentence: local testing saves debugging time.");
Run it with node test.js while you tweak prompts, headers, or parsing logic. This loop is faster than round-tripping through your full application every time.
Mock responses for unit tests
Hitting the real API in every unit test run is slow, costs money, and makes tests flaky when the network is down. Once you know your request/response shape works (via the curl and harness steps above), mock it for your test suite.
import { vi, test, expect } from "vitest";
test("parses Claude response correctly", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ type: "text", text: "mocked reply" }],
usage: { input_tokens: 10, output_tokens: 5 },
}),
});
const result = await callClaude("test prompt");
expect(result).toBe("mocked reply");
});
Reserve real API calls for a small set of integration tests that run less often (e.g., on merge to main), and use mocks everywhere else. This is the standard pattern for testing any third-party API locally, not just Claude's.
Test streaming and tool use separately
Streaming and tool use behave differently from plain completions and deserve their own local test scripts, since bugs here (dropped chunks, malformed tool inputs) are easy to miss with a single happy-path test.
For streaming, verify you're correctly handling event: content_block_delta chunks and the final message_stop event — a common bug is closing the connection or parser before the stream actually ends.
For tool use, send a request with a tools array and confirm your code correctly extracts the tool_use block, executes the tool locally, and sends the result back in a follow-up message with the right tool_result format.
If you're building against SubToAPI, both are documented with working request/response examples at /docs/streaming and /docs/tools — useful as a reference when your local test output doesn't match what you expect.
Test error handling and retries
Local testing is also the right place to verify your app handles 429s, timeouts, and malformed responses gracefully — not just the happy path. Simulate a rate limit by mocking a 429 response and confirming your retry logic backs off correctly rather than looping immediately.
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 429,
text: async () => "rate limited",
});
Catching this locally is far cheaper than discovering your retry loop hammers the API in production.
When local testing gets slow: use one endpoint for everything
If your team juggles multiple Anthropic keys, rate limits, and billing dashboards just to run local tests, that overhead adds up. SubToAPI gives every application its own key, unified usage metadata per request, and one dashboard for the whole team — so local test runs, staging, and production all go through the same predictable interface. Check /pricing for plan details or start a free trial at /signup if you want to simplify this part of the workflow.
FAQ
Can I run the Claude API entirely offline? No — there's no local server binary for Claude. "Local testing" means testing your integration code against the real API (or mocked responses) from your machine, not running the model itself locally.
What's the fastest way to check if my API key works? Run a single curl request with a short prompt and max_tokens set low. A successful JSON response confirms your key, headers, and network path are correct in seconds.
Should unit tests call the real Claude API? Generally no. Mock the API response shape for fast, reliable unit tests, and reserve a small number of real API calls for integration tests that run less frequently.