Claude API Curl Example: Requests, Headers & Errors
If you want to test the Claude API without writing a script, curl is the fastest way to do it. This article gives you a working curl example you can copy, paste, and run right now, plus the header and body details that trip people up the first time.
The short version: you send a POST request to the messages endpoint with an API key header, a version header, and a JSON body containing the model name, a max token limit, and a messages array. Below is the full breakdown, including streaming and common errors.
Minimal curl request
Here's a bare-bones example that sends a single user message and prints the response:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-20250514",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Explain what a race condition is in one paragraph."}
]
}'
Three headers are required every time:
x-api-key— your Anthropic API key.anthropic-version— a date string that pins the API version you're targeting.content-type— alwaysapplication/jsonfor this endpoint.
The body needs at minimum model, max_tokens, and messages. Leave out max_tokens and you'll get a validation error — it's not optional like it is in some other APIs.
Adding a system prompt
System instructions go in a top-level system field, not inside the messages array:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-20250514",
"max_tokens": 1024,
"system": "You are a terse code reviewer. Answer in bullet points only.",
"messages": [
{"role": "user", "content": "Review this function for bugs: def add(a, b): return a - b"}
]
}'
Multi-turn conversations
Claude's API is stateless — there's no server-side conversation memory. Each request needs the full message history, alternating user and assistant roles:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-20250514",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "What is the capital of Portugal?"},
{"role": "assistant", "content": "Lisbon."},
{"role": "user", "content": "What is its population?"}
]
}'
If you skip this and only send the latest message, Claude has no idea what "its" refers to.
Streaming with curl
Add "stream": true to the body and use curl -N to disable output buffering so you see tokens as they arrive instead of all at once at the end:
curl -N https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-20250514",
"max_tokens": 1024,
"stream": true,
"messages": [
{"role": "user", "content": "Write a haiku about deadlines."}
]
}'
The response comes back as server-sent events — a sequence of event: and data: lines you parse incrementally rather than a single JSON blob.
Common curl errors and what they mean
401 authentication_error— the key is missing, malformed, or revoked. Check the header name is exactlyx-api-key, notAuthorization.400 invalid_request_error: max_tokens— you forgotmax_tokensor set it too high for the model.429 rate_limit_error— you're hitting your organization's request or token-per-minute cap.overloaded_error— the API is temporarily saturated; retry with backoff.- Empty response with
stream: trueand no-N— curl buffered the whole thing, it'll print once complete instead of live.
Using curl against SubToAPI instead
If you already pay for Claude through a subscription and want an HTTPS API without separately provisioning Anthropic API billing, SubToAPI exposes the same request shape under its own endpoint and key format. The curl call looks almost identical — swap the host and the auth header:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-20250514",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Summarize the tradeoffs of REST vs gRPC."}
]
}'
Instead of x-api-key, SubToAPI uses a standard Authorization: Bearer sub_live_... header, since that's the pattern most HTTP clients and tooling expect out of the box. Streaming, tool use, and usage metadata work the same way as the native API — see /docs/messages and /docs/streaming for the full parameter list. If you're setting this up for the first time, /docs/quickstart walks through generating a key and making your first request in under five minutes. Plans start with a free trial at /signup, and pricing details are at /pricing.
Turning curl into a script
Once the curl command works, wrapping it in a shell script or Makefile target is often enough for internal tooling — no SDK required:
#!/usr/bin/env bash
set -euo pipefail
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "{
\"model\": \"claude-opus-4-20250514\",
\"max_tokens\": 1024,
\"messages\": [{\"role\": \"user\", \"content\": \"$1\"}]
}" | jq -r '.content[0].text'
Save that as ask.sh, run chmod +x ask.sh, and call ./ask.sh "your question" to get a plain-text answer piped through jq.
Questions
Why do I need anthropic-version if I'm not calling Anthropic's own API? It's specific to Anthropic's endpoint versioning scheme. Gateways like SubToAPI that proxy the same request format don't require it since they manage versioning internally.
Can I use curl for tool use / function calling? Yes — add a tools array to the request body with JSON schema definitions. The response and header structure stay the same as a basic message request; see /docs/tools for the full format.
Why does my curl command hang or return nothing? Usually a missing -N flag with streaming enabled, a firewall blocking outbound HTTPS, or a key that's valid but has zero remaining quota — check the response status code first with -i before assuming it's a network issue.