How to Migrate From OpenAI to Claude API
Migrating from the OpenAI API to the Claude API mostly comes down to three changes: how you authenticate, how you structure the request body, and how you handle the response shape. The core workflow — send messages, get a completion, optionally stream or call tools — is conceptually the same, so most teams can finish the migration in an afternoon if they isolate the model call behind a single function.
This guide walks through the concrete differences you'll hit in code: endpoint and headers, the messages format, system prompts, streaming events, function/tool calling, and error handling. Each section includes a before/after code snippet so you can diff your existing OpenAI integration against the Claude equivalent.
Authentication and endpoint
OpenAI uses a single Authorization: Bearer header against https://api.openai.com/v1/chat/completions. Claude's native API uses a different header name and a required version header:
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 '{...}'
If you'd rather keep the Authorization: Bearer pattern you already have wired into your HTTP client, SubToAPI exposes Claude through a standard OpenAI-style bearer token instead:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{...}'
That one header swap is often the only infrastructure change needed if your codebase already assumes bearer auth, rate-limit headers, and per-key usage tracking. See /docs/quickstart for the full setup.
Request body differences
The biggest structural difference is the system prompt. OpenAI puts it inside the messages array as a message with role: "system". Claude takes it as a separate top-level system field:
// OpenAI
{
"model": "gpt-4o",
"messages": [
{ "role": "system", "content": "You are a helpful support agent." },
{ "role": "user", "content": "How do I reset my password?" }
]
}
// Claude
{
"model": "claude-sonnet-4-5",
"system": "You are a helpful support agent.",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "How do I reset my password?" }
]
}
Two things to note: Claude requires max_tokens on every request (OpenAI defaults it if omitted), and Claude's messages array must strictly alternate user and assistant roles — no consecutive messages from the same role. If your conversation history logic ever merges or skips turns, add a normalization step before sending.
Response shape
OpenAI returns choices[0].message.content as a string. Claude returns a content array made of typed blocks, since a single response can mix text and tool calls:
{
"id": "msg_01...",
"role": "assistant",
"content": [
{ "type": "text", "text": "You can reset your password from Settings > Security." }
],
"stop_reason": "end_turn",
"usage": { "input_tokens": 24, "output_tokens": 18 }
}
In code, that means swapping response.choices[0].message.content for something like:
const text = response.content
.filter(block => block.type === "text")
.map(block => block.text)
.join("");
Token usage is also reported differently: input_tokens / output_tokens instead of prompt_tokens / completion_tokens. If you bill customers based on usage, update your metering logic accordingly — SubToAPI surfaces this per-key usage automatically in the dashboard so you don't have to parse it yourself; see /docs/messages for the full response reference.
Streaming
Both APIs support server-sent events, but the event structure differs. OpenAI streams chat.completion.chunk objects with incremental delta.content strings. Claude streams named events: message_start, content_block_start, content_block_delta, content_block_stop, and message_stop.
const stream = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"content-type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Write a haiku about migration." }]
})
});
You'll need to parse content_block_delta events and concatenate delta.text fields rather than reading a single delta.content key. Full event reference is at /docs/streaming.
Tool and function calling
OpenAI calls them "functions" or "tools" with a tool_calls array in the response. Claude calls them "tools" too, but the schema and response format differ slightly:
{
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": { "location": { "type": "string" } },
"required": ["location"]
}
}
]
}
When Claude wants to call a tool, it returns a content block with type: "tool_use" instead of a top-level tool_calls array, and you respond with a tool_result content block in your next user message rather than a role: "tool" message. If your app has non-trivial tool-use logic, budget extra time here — it's the part most likely to need real code changes rather than a find-and-replace. Details and working examples are in /docs/tools.
A practical migration checklist
- Isolate all OpenAI calls behind one wrapper function if they aren't already.
- Move system prompts out of the messages array into the
systemfield. - Add a message-normalization step to guarantee strict user/assistant alternation.
- Rewrite response parsing to read from the
contentarray instead ofchoices[0].message. - Update streaming event handlers to Claude's named SSE events.
- Rewrite tool schemas to use
input_schemaand handletool_use/tool_resultblocks. - Update usage/billing code to read
input_tokens/output_tokens. - Swap your auth header — or point your base URL at SubToAPI and keep bearer-token auth, team seats, and per-key usage dashboards without touching your HTTP client. Start at /signup or compare plans at /pricing.
Doing this behind a single wrapper function means the rest of your application — prompt templates, business logic, UI — doesn't need to know which provider is underneath, which also makes a future A/B test between models much easier.
Questions
Do I need to rewrite my entire prompt library when switching from OpenAI to Claude? No. Most prompts transfer with minor edits — mainly moving system instructions into the system field and double-checking message role alternation. Model-specific prompting quirks (verbosity, formatting tendencies) may need light tuning after the migration.
Can I keep using OpenAI-style bearer token authentication with Claude? Yes, if you use a layer like SubToAPI, which exposes Claude through standard Authorization: Bearer keys, so your existing HTTP client configuration doesn't need to change — see /docs/quickstart.
What's the most error-prone part of migrating tool/function calling? Response parsing. OpenAI's tool_calls array and Claude's tool_use content blocks are structurally different, and the result message format for returning tool output back to the model also differs. Test this path thoroughly before shipping; see /docs/tools for working examples.