← Blog

How to Migrate From OpenAI to Claude API

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

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

  1. Isolate all OpenAI calls behind one wrapper function if they aren't already.
  2. Move system prompts out of the messages array into the system field.
  3. Add a message-normalization step to guarantee strict user/assistant alternation.
  4. Rewrite response parsing to read from the content array instead of choices[0].message.
  5. Update streaming event handlers to Claude's named SSE events.
  6. Rewrite tool schemas to use input_schema and handle tool_use/tool_result blocks.
  7. Update usage/billing code to read input_tokens/output_tokens.
  8. 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.

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 →