Claude Max 플랜으로 API 호출하면 429가 뜨는 이유 — 인증 체계 5단계 완전 정리

Claude Max 플랜의 OAuth 토큰(sk-ant-oat)으로 Messages API를 직접 호출하면 429 Rate Limit이 뜹니다. Claude Code의 인증 우선순위 5단계, sk-ant-oat vs sk-ant-api 차이, 그리고 스크립트에서 Max 플랜을 활용하는 우회법을 실제 트러블슈팅 사례와 함께 정리합니다.


💡 Tip. 바쁜 현대인들을 위한 본문 요약

Claude Max 플랜($100/월)을 구독 중인데, Messages API를 직접 호출하면 429 Rate Limit 에러가 뜬다. 이유는 Max 플랜의 OAuth 토큰(sk-ant-oat)은 CLI 전용이고, 직접 API 호출에는 Console API 키(sk-ant-api)가 별도로 필요하기 때문이다. 이 글에서는 Claude Code의 인증 우선순위 5단계, 두 토큰의 차이, 그리고 Max 플랜만으로 스크립트를 돌리는 우회법을 정리한다.


문제 상황: Max 플랜인데 API가 거부한다

블로그 자동화 파이프라인에서 키워드 필터링용으로 Anthropic Messages API를 직접 호출했다. fetchapi.anthropic.com/v1/messages에 요청을 보내는 간단한 코드다.

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.ANTHROPIC_API_KEY, // ← 여기가 문제
    "anthropic-version": "2023-06-01",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-6",
    max_tokens: 4096,
    messages: [{ role: "user", content: prompt }],
  }),
});

Max 플랜을 구독 중이니까 당연히 될 줄 알았다. 결과는:

{
  "type": "error",
  "error": {
    "type": "rate_limit_error",
    "message": "Error"
  }
}

HTTP 429. Rate limit에 걸렸다.

첫 요청부터 429라는 게 이상했다. Rate limit이면 여러 번 호출한 뒤에 걸려야 하는데, 단 1회 호출에 즉시 거부당했다. 이건 rate limit이 아니라 인증 자체가 거부된 것이다.


원인 분석: 토큰이 두 종류다

모니터를 노려보며 분석: 토큰이 두 종류다 디버깅 중
모니터를 노려보며 분석: 토큰이 두 종류다 디버깅 중

macOS Keychain에 저장된 토큰을 확인해봤다.

echo "$ANTHROPIC_API_KEY" | head -c 15
# sk-ant-oat01-...

sk-ant-oat. 이게 범인이었다.

Anthropic의 API 토큰은 프리픽스로 용도가 구분된다.

프리픽스정식 명칭발급처용도
sk-ant-apiConsole API Keyconsole.anthropic.comREST API 직접 호출 (x-api-key 헤더)
sk-ant-oatSubscription OAuth TokenClaude.ai 로그인 (Pro/Max)Claude Code CLI 전용 (내부 인증)

Max 플랜의 OAuth 토큰은 CLI를 통해서만 유효하다. claude -p 명령을 실행하면 CLI 내부에서 이 토큰으로 Anthropic 서버와 통신하지만, 이 토큰을 꺼내서 x-api-key 헤더에 직접 넣으면 서버가 거부한다.


Claude Code 인증 우선순위 5단계

직접 정리한 Claude Code 인증 우선순위 흐름도
직접 정리한 Claude Code 인증 우선순위 흐름도

Claude Code 공식 문서에 명시된 인증 우선순위(Authentication Precedence)는 다음과 같다.

Claude Code Authentication — 공식 문서
인증 방식 5단계 우선순위, Keychain 저장 구조, apiKeyHelper 등 인증 체계 전체 설명.
code.claude.com
우선순위인증 방식환경변수/설정API 직접 호출
1Cloud ProviderCLAUDE_CODE_USE_BEDROCK✅ (해당 클라우드 API)
2Auth TokenANTHROPIC_AUTH_TOKEN⚠️ (프록시/게이트웨이)
3API KeyANTHROPIC_API_KEY (sk-ant-api)직접 호출 가능
4apiKeyHelper스크립트 출력값✅ (스크립트가 API 키 반환 시)
5Subscription OAuth/login으로 발급 (sk-ant-oat)CLI 전용

핵심은 3번과 5번의 차이다.

  • 3번 ANTHROPIC_API_KEY: Console에서 발급한 정식 API 키. x-api-key 헤더로 api.anthropic.com에 직접 요청 가능. 사용한 만큼 과금 (종량제).
  • 5번 Subscription OAuth: Max/Pro 구독 시 /login으로 자동 발급되는 토큰. CLI 내부에서만 유효. 직접 API 호출 불가.

그리고 공식 문서에는 이런 경고가 있다:

If you have an active Claude subscription but also have ANTHROPIC_API_KEY set in your environment, the API key takes precedence once approved. This can cause authentication failures if the key belongs to a disabled or expired organization.

환경변수에 API 키가 있으면 구독 토큰보다 우선한다. 즉, 두 인증이 공존하면 예상과 다르게 동작할 수 있다.


해결: 세 가지 선택지

세 가지 선택지 수정 완료, 이제 좀 살 것 같다
세 가지 선택지 수정 완료, 이제 좀 살 것 같다

선택지 A: Console API 키 발급 (종량제)

가장 정석적인 방법. console.anthropic.com에서 API 키를 발급받으면 직접 호출이 가능하다.

# Console에서 발급한 키
export ANTHROPIC_API_KEY="sk-ant-api03-..."

# 직접 호출 가능
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-sonnet-4-6", "max_tokens": 1024, "messages": [{"role":"user","content":"Hello"}] }'

비용: Sonnet 4.6 기준 input $3/MTok, output $15/MTok. 키워드 필터 1회 호출이면 ~$0.01 수준.

단점: Max 플랜과 별도 과금. 월 $100 내고 있는데 또 돈이 나간다.

선택지 B: claude -p subprocess 래퍼 (무료)

Max 플랜의 사용량 안에서 처리하려면, CLI를 subprocess로 호출해야 한다.

import { spawn } from "node:child_process";

function callClaude(prompt: string, systemPrompt: string): Promise<string> {
  return new Promise((resolve, reject) => {
    // ⚠️ 환경변수 정리 — 중첩 세션 방지
    const env = { ...process.env };
    delete env.ANTHROPIC_API_KEY;      // API 키가 있으면 구독 대신 이걸 씀
    delete env.CLAUDE_CODE_USE_BEDROCK; // 클라우드 프로바이더 우선순위 차단

    const child = spawn("claude", [
      "-p",
      "--model", "claude-sonnet-4-6",
      "--system-prompt", systemPrompt,
    ], { env, stdio: ["pipe", "pipe", "pipe"] });

    const chunks: Buffer[] = [];
    child.stdout.on("data", (c) => chunks.push(c));
    child.stdin.write(prompt);
    child.stdin.end();

    child.on("close", (code) => {
      if (code !== 0) reject(new Error(`exit ${code}`));
      else resolve(Buffer.concat(chunks).toString("utf-8"));
    });
  });
}

주의사항이 3가지 있다.

주의내용
환경변수 정리ANTHROPIC_API_KEY가 환경에 있으면 CLI가 구독 대신 해당 키를 사용한다. subprocess에서는 명시적으로 삭제해야 한다
stdin pipe hang프롬프트가 길면(8KB+) stdin pipe가 불안정해질 수 있다. 타임아웃 필수
중첩 세션 차단OpenClaw 등 에이전트 프레임워크 내부에서 claude -p를 호출하면 CLAUDE_CODE_* 환경변수가 중첩 세션을 유발한다. 전부 삭제해야 한다

선택지 C: claude auth login --console (전환)

CLI의 인증 자체를 Console 기반으로 전환하는 방법.

claude auth login --console

이렇게 하면 CLI가 구독 OAuth 대신 Console API 키로 인증한다. 이후 claude -p로 호출해도 API 종량 과금이 된다. Max 플랜의 무제한 사용량을 포기하는 셈이므로 신중하게 선택해야 한다.


비교 정리

몇 시간 삽질 끝에 비교 정리 해결 완료
몇 시간 삽질 끝에 비교 정리 해결 완료

Console API (A)subprocess (B)Console 전환 (C)
직접 fetch 호출
Max 플랜 사용량❌ 별도 과금✅ 구독 내❌ 별도 과금
안정성✅ 높음⚠️ stdin hang 위험✅ 높음
비용종량제무료종량제
구현 난이도낮음중간낮음

실전에서 내가 선택한 방법

나는 B + 타임아웃 폴백 조합을 선택했다.

claude -p (120초 제한) → 성공: 정상 처리
                       → 실패: 전체 키워드 통과 (폴백)

이유는 단순하다.

  1. 이미 Max 플랜을 쓰고 있다. 별도 API 비용을 내고 싶지 않다.
  2. 필터 실패 시 치명적이지 않다. AI 필터가 실패하면 키워드가 전부 통과되지만, 뒤에 중복 제거 단계가 있어서 품질이 크게 떨어지지 않는다.
  3. 타임아웃을 120초로 설정했다. claude -p가 hang되면 120초 후 SIGTERM → 3초 후 SIGKILL로 확실하게 종료한다.
// SIGTERM 후 3초 뒤 SIGKILL — hang된 프로세스 확실히 제거
const timer = setTimeout(() => {
  child.kill("SIGTERM");
  setTimeout(() => child.kill("SIGKILL"), 3000);
  reject(new Error("타임아웃"));
}, 120_000);

이 구조로 크론 잡이 블로킹되지 않으면서, Max 플랜의 사용량 안에서 무료로 LLM을 활용할 수 있게 됐다.


핵심 정리

몇 시간 삽질 끝에 핵심 정리 해결 완료
몇 시간 삽질 끝에 핵심 정리 해결 완료

토큰 프리픽스발급처직접 API 호출CLI 사용과금
sk-ant-apiConsole✅ (우선순위 3)종량제
sk-ant-oatClaude.ai 로그인❌ (429)✅ (우선순위 5)구독 내

기억할 것 하나: Max 플랜 ≠ API 무제한. Max 플랜은 CLI와 웹에서만 유효하고, REST API 직접 호출은 별도 Console API 키가 필요하다.