Claude API Migration from OpenAI: A Practical Guide
Migrating from the OpenAI API to the Claude API is mostly a matter of translating request shapes and a handful of conceptual differences — it's not a rewrite. If you're searching for a Claude API migration from OpenAI guide, you're probably already running a product on chat.completions and want to know exactly what changes: field names, system prompt handling, streaming events, and tool calling.
This guide walks through the concrete differences, gives you working code for both sides, and flags the gotchas that trip people up (role structure, token counting, and response parsing being the big three).
Why teams migrate
The usual reasons: better reasoning on long documents, more reliable tool use, longer context windows, or simply wanting a second model provider so you're not locked into one vendor. Whatever the reason, the migration itself is a few hours of work for a typical chat or completion endpoint, not a few weeks.
Authentication differences
OpenAI uses a single Authorization: Bearer sk-... header. Claude's native API uses x-api-key plus an anthropic-version header instead of Bearer auth:
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 your backend (or your billing setup) is built around standard Bearer-token auth, this is one of the smaller things worth normalizing. SubToAPI wraps Claude access behind a standard Authorization: Bearer sub_live_... key, so if you're migrating an OpenAI-shaped codebase, you can keep the auth pattern identical and skip the header rewrite:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-4","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'
Endpoint and request shape
OpenAI's chat endpoint is /v1/chat/completions. Claude's is /v1/messages. The request body is similar but not identical:
OpenAI:
{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quicksort."}
],
"max_tokens": 1024
}
Claude:
{
"model": "claude-sonnet-4",
"system": "You are a helpful assistant.",
"messages": [
{"role": "user", "content": "Explain quicksort."}
],
"max_tokens": 1024
}
The key difference: Claude pulls the system prompt out of the messages array into a top-level system field. This is the single most common migration bug — people leave a {"role": "system", ...} entry inside messages and Claude rejects it. Move it out.
max_tokens is also required on every Claude request, not optional, so don't skip it if your OpenAI code treated it as a nice-to-have.
Message roles and alternation
OpenAI tolerates multiple consecutive messages from the same role and a flexible message history. Claude's messages array expects alternating user/assistant turns, starting with user. If your migration involves replaying stored conversation history, add a normalization step that merges consecutive same-role messages or drops stray leading assistant entries before sending the request. This is a cheap function to write once and reuse everywhere.
Response parsing
OpenAI returns choices[0].message.content as a string. Claude returns a content array of typed blocks:
{
"id": "msg_01...",
"content": [
{"type": "text", "text": "Quicksort is a divide-and-conquer algorithm..."}
],
"stop_reason": "end_turn",
"usage": {"input_tokens": 24, "output_tokens": 180}
}
If you were doing response.choices[0].message.content, you now need response.content.map(b => b.text).join("") or, more robustly, filter for type === "text" since tool-use responses mix in other block types. See /docs/messages for the full response schema.
Streaming differences
OpenAI streams data: chunks with a delta.content string. Claude's streaming protocol emits named server-sent events — message_start, content_block_start, content_block_delta, content_block_stop, message_stop — each with its own JSON payload. Your streaming parser needs to switch on event.type rather than assuming every chunk is a content delta:
for await (const event of stream) {
if (event.type === "content_block_delta") {
process.stdout.write(event.delta.text);
}
}
This is a bigger rewrite than the request body change if your frontend has a lot of streaming logic baked in. Full event reference is in /docs/streaming.
Tool calling / function calling
Both APIs support structured tool use but with different naming and response shapes. OpenAI uses tools with function.parameters; Claude uses tools with input_schema, and returns tool calls as tool_use content blocks rather than a separate tool_calls field:
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}
The round trip — detecting a tool_use block, running your function, and sending the result back as a tool_result content block in the next user message — is logically the same pattern as OpenAI's function calling, just with different field names. Walk through a worked example in /docs/tools before you port a tool-heavy agent.
Token counting and billing
OpenAI and Claude tokenize differently, so any cost estimation or usage-tracking code that assumes OpenAI's tokenizer will be off. Both APIs return usage in the response (usage.prompt_tokens/completion_tokens vs usage.input_tokens/output_tokens) — rely on the returned numbers rather than estimating client-side.
A practical migration checklist
- Extract system prompts out of the messages array into the
systemfield. - Normalize message history to strict user/assistant alternation.
- Update response parsing to handle the
contentblock array. - Rewrite the streaming event handler around named SSE event types.
- Remap tool schemas from
function.parameterstoinput_schemaand update tool-call response handling. - Swap token/cost tracking to use the response's
usageobject, not a shared estimator. - Re-run your prompt eval suite — temperature and instruction-following behavior differ enough that prompts often need small adjustments.
If you want to skip steps 1–2 of the infrastructure side (key management, team billing, usage dashboards) and keep a familiar Bearer-token, JSON-over-HTTPS integration pattern, /docs/quickstart has a working example you can drop in alongside your existing OpenAI client code, and /pricing covers the Solo, Team, and Scale plans if you're rolling this out to a team.
Questions
Do I need to change my HTTP client library? No. Both APIs are plain JSON over HTTPS, so your existing fetch/axios/requests setup works — only the request body, headers, and response parsing change.
Will my prompts behave the same on Claude as on OpenAI? Mostly, but not identically. Claude tends to follow structured instructions and system prompts more literally, so re-test prompts that relied on OpenAI-specific phrasing quirks rather than assuming a drop-in behavioral match.
Can I run OpenAI and Claude side by side during migration? Yes — many teams route a percentage of traffic to Claude first to compare quality and latency before fully cutting over, which is easier if your auth and client code stay in a consistent Bearer-token shape across both providers.