← Blog

Claude API vs OpenAI API: A Practical Migration Guide

2026-10-12 · 5 min read · SubToAPI Team

Migrating between the Claude API and the OpenAI API mostly comes down to three things: the request/response shape, how system prompts and roles are handled, and how streaming and tool calls are structured. The underlying concepts — chat messages, system instructions, function calling, streaming tokens — map closely between the two, but the field names, auth headers, and a few behavioral details differ enough to break a naive copy-paste port.

This guide walks through the concrete differences you'll hit migrating code in either direction, with working examples for both directions, so you can update your integration without re-reading two full API references from scratch.

Authentication differences

OpenAI uses a single bearer token in the Authorization header:

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'

Claude's native API uses a custom header instead of Authorization, plus 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 '{"model":"claude-opus-4","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}'

That anthropic-version header and the different auth header name are the first thing most migrations miss — you'll get a 401 or 400 that doesn't obviously point at "wrong header."

If you're routing through SubToAPI instead, this part gets simpler: it uses a standard Authorization: Bearer $SUBTOAPI_KEY header like OpenAI does, which removes one point of friction when teams are used to OpenAI's auth conventions. See /docs/quickstart for setup.

Request body: the key structural differences

System prompts. OpenAI accepts a system role as part of the messages array. Claude's native API has system as a top-level field, separate from messages:

// OpenAI style
{
  "messages": [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Explain TCP handshakes."}
  ]
}
// Claude style
{
  "system": "You are a helpful assistant.",
  "messages": [
    {"role": "user", "content": "Explain TCP handshakes."}
  ]
}

If you're porting a message history, you need to extract the first system message out of the array and move it into its own field.

max_tokens is required. OpenAI's max_tokens (or max_completion_tokens depending on model) is optional in most cases. Claude's /v1/messages requires max_tokens on every request — omit it and you get a validation error. Budget for this explicitly rather than relying on a default.

Content as structured blocks. OpenAI's content field is typically a plain string (or an array for multimodal). Claude's content is more consistently an array of typed blocks, even for simple text:

{"role": "user", "content": [{"type": "text", "text": "Explain TCP handshakes."}]}

A plain string also works for simple text-only messages in Claude's API, but once you add images or tool results you'll need the block format, so it's worth standardizing on it early.

Streaming differences

OpenAI streams via server-sent events with a delta.content field per chunk and a final [DONE] marker. Claude streams a sequence of named event types — message_start, content_block_delta, message_delta, message_stop — each with its own payload shape:

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

This means your stream parser needs to switch on event.type rather than assume every chunk is a content delta. If your OpenAI integration just concatenates delta.content in a loop, that logic needs to be rewritten to handle Claude's event taxonomy, or to handle SubToAPI's streaming responses, which follow this same event-based pattern — see /docs/streaming for the full event reference.

Tool use / function calling

Conceptually identical, syntactically different. OpenAI defines tools with a function wrapper and returns tool_calls on the assistant message. Claude defines tools directly with input_schema (not parameters) and returns tool_use content blocks:

// Claude tool definition
{
  "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 of {"type": "tool_use", "id": "...", "name": "get_weather", "input": {...}}. You respond with a tool_result content block referencing that id, inside a new user message — rather than OpenAI's separate tool role. This is the part of a migration most likely to need a real rewrite rather than a find-and-replace, since the conversation threading model is different. SubToAPI exposes the same tool-use flow against the Messages endpoint, documented at /docs/tools, if you want one reference while porting both directions.

Model names and parameters

There's no 1:1 model mapping — you'll need to pick a Claude model tier (Haiku, Sonnet, Opus) based on latency/cost/quality tradeoffs rather than assuming "the GPT-4o equivalent" exists by name. Parameter names mostly carry over conceptually (temperature, top_p) but max_tokens behaves differently as noted above, and Claude doesn't have an exact equivalent to OpenAI's n (multiple completions per request) — you'd issue separate requests instead.

A practical migration checklist

If you're consolidating multiple provider integrations into one HTTPS surface rather than maintaining separate SDKs, SubToAPI puts Claude access behind application API keys (sub_live_...) with the same streaming and tool-use model described here, plus usage metadata and per-key scoping — see /docs/messages for the request/response reference and /pricing for plan details.

Questions

Does Claude support the OpenAI-compatible endpoint format? No — Claude's native API uses its own /v1/messages schema with a top-level system field, required max_tokens, and a different streaming event model. There's no official OpenAI-compatible shim from Anthropic, so a real migration requires adapting your request/response handling.

What's the biggest source of bugs when porting OpenAI code to Claude? Tool use and multi-turn tool conversations. The tool_use / tool_result content-block threading model differs structurally from OpenAI's tool role messages, and it's easy to get the conversation history wrong in ways that only surface on the second or third tool call.

Can I keep one codebase that talks to both APIs? Yes, by writing a thin adapter layer that normalizes system prompts, content blocks, and streaming events into one internal format before they hit provider-specific request builders. Routing Claude calls through a layer like SubToAPI, which mirrors OpenAI's bearer-token auth convention, can also reduce the auth-layer differences you need to abstract.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →