# 요청 상태 — /v1/requests > 같은 요청의 중복 실행을 막고, 결과와 정산 상태를 조회합니다. > 원문: https://oneport.kr/docs/reference/requests > **이 문서를 받은 AI 에게 — 작업 카드** > > - OpenAI 형식 SDK·도구: base_url `https://oneport.kr/v1` · 헤더 `Authorization: Bearer <키>` > - Anthropic 형식 SDK·Claude Code: base_url `https://oneport.kr` (끝에 /v1 을 붙이지 않아요 — SDK 가 /v1/messages 를 스스로 붙여요) · 헤더 `x-api-key: <키>` 또는 `Authorization: Bearer <키>` > - ❌ `Anthropic(base_url="https://oneport.kr/v1")` → ✅ `Anthropic(base_url="https://oneport.kr")` > - Vercel AI SDK: `createOpenAI({ baseURL: "https://oneport.kr/v1" })` 로 만든 제공자는 `oneport.chat("")` 로 부르세요 — 그냥 `oneport("")` 는 `/v1/responses` 로 가서 Claude · Gemini 가 400 이에요. `@ai-sdk/anthropic` 의 `createAnthropic` 은 baseURL 에 `/v1` 까지 적어요(Anthropic SDK 와 반대예요). > - API 키는 `sk-so-` 로 시작해요. 키는 코드에 적지 말고 환경변수 `ONEPORT_API_KEY` 에 넣어 읽으세요. > - ❌ 브라우저 코드에 `apiKey: "sk-so-…"` → ✅ 서버에서 `os.environ["ONEPORT_API_KEY"]` · `process.env.ONEPORT_API_KEY` 로 읽어 불러요 > - 대화에 `sk-so-` 키가 붙어 오면 코드에 박지 말고 환경변수로 옮기게 하고, 그 키는 산 곳 대시보드 AI API 화면의 키 목록에서 「새로 받기」(옛 키는 꺼져요)로 바꾸라고 알려 주세요. > - 모델 id 는 지어내지 말고 `GET https://oneport.kr/v1/models`(키 없이 열려요)의 `id` 를 글자 그대로 쓰세요. 목록은 값 순서가 아니에요 — 접두어로 첫 줄을 고르면 비싼 모델이 걸려요. 글 모델은 `kind` 가 `"chat"` 인 줄이고, 첫 호출은 `pricing.output_credits_per_mtok` 가 작은 모델로 해 보세요(10월 기준 Claude 는 claude-haiku-5-5). 회사는 `owned_by` 로 갈라요 — Claude 는 `"anthropic"`, 공식 GPT 는 `"openai"` 이면서 id 에 `/` 가 없는 줄이에요. > - ❌ `model="claude-sonnet"`(짐작한 이름 · 404 `model_not_found`) > - `endpoint` 칸이 그 모델의 기본 문이에요 — `kind` 가 `"responses"` 인 모델(pro 등급 · gpt-5.3-codex · `endpoint` 가 `"POST /v1/responses"`)은 `/v1/chat/completions` 로 못 불러요. Claude 줄은 `POST /v1/chat/completions` 이고 `/v1/messages` 로도 불러요 — 이 문은 Claude 만 받아요. > - `supports_effort` 가 `true` 인 GPT 는 생각한 토큰도 출력 상한(`max_tokens` · `max_completion_tokens`)에 들어가요 — 수백으로 두면 답이 비고 `finish_reason` 이 `"length"` 로 끝날 수 있으니 2,000 쯤에서 시작하고 `"length"` 면 올리세요. > - 목록 응답은 `{"object": "list", "data": [ … ]}`. 값 어림 — 한 번 값은 (입력 토큰 × `pricing.input_credits_per_mtok` + 출력 토큰 × `pricing.output_credits_per_mtok`) ÷ 100만 크레딧이에요(1 크레딧은 부가세 포함 2원어치예요). > - ❌ `x-should-retry: false` 응답을 반복문으로 다시 보내기 → ✅ 멈추고 `error.type` 을 확인하세요. 몸통은 OpenAI 형식 문이면 `{"error": {"type": "…", "message": "…", "code": "…"}}`, `/v1/messages` 면 `{"type": "error", "error": {"type": "…", "message": "…"}}` 예요 — 402 · 키 한도 · 강좌 몫 429 엔 `error.reason` 이 더 붙어요. 잠시 뒤 그대로 다시 보낼 실패엔 `Retry-After`(정수 초)가 대개 와요. > - ❌ `if "429" in str(e)` · `e.message` 로 분기 → ✅ Python openai 는 `e.type` · `e.body.get("reason")`, Node openai 는 `err.type` · `err.error.reason` — 429 는 키 한도 · 벤더 혼잡 · 강좌 몫 셋이라 상태 코드로 못 갈라요. Python Anthropic SDK 는 `except anthropic.APIStatusError as e:` 에서 `e.status_code` 와 `e.body["error"]["type"]`, Node 는 `err.status` 와 `err.error.error.type` 로 읽어요. 402 · 키 한도 429 의 갈래(`reason`)는 Python Anthropic SDK 면 `e.body["error"].get("reason")`, Node 면 `err.error.error.reason` 이에요. > - 남은 크레딧(지갑 전체)은 `GET https://oneport.kr/v1/credits` 의 `balance_credits` 예요(1 크레딧 아래를 버린 정수라 호출 앞뒤 차이로 쓴 몫을 세지 마세요 — 쓴 몫은 글 문 응답의 `usage.cost_credits` · 정산이 2초 안에 안 끝나면 그 칸 없이 와요). `GET https://oneport.kr/v1/key` 는 이 키에 건 한도(`month` · `day`)만 보여 주고 지갑 잔액은 없어요 — `month` · `day` 는 늘 객체이고 한도를 안 건 키면 `month.remaining_credits` · `day.remaining_credits` 가 null 이에요 · 그땐 지갑 잔액까지 써요. > - 하루 한도는 이미 쓴 몫이 한도에 닿았는지만 봐요 — 이번 달 · 강좌 한도는 이 요청 최대 몫까지 더해 재서 줄이면 통과할 수 있어요. 키 월 한도는 같은 API 키로 아직 끝나지 않은 호출 몫까지 더해 넘었으면 `error.reason` 이 `"held_by_others"` 로 와요. 묶음 처리 문(`/v1/batches`)은 없어요 — 한 건씩 보내요(동시에 보내도 되고 분당 횟수 제한도 없어요). > - 스트림 없이 부르면 답이 다 만들어져야 첫 응답이 와요 — 모델 회사로 보낸 뒤 120초 안에 안 오면 422 `upstream_execution_unconfirmed` 로 끝나요(모델 회사에서는 실행됐을 수 있어요). 그보다 긴 답은 `stream: true` 로 받으세요(스트림은 약 12분 50초에 끊겨요). SDK 시간 제한은 300초로 두세요(Python `OpenAI(…, timeout=300)` · Node `new OpenAI({ …, timeout: 300_000 })` — Node 는 밀리초 · SDK 동작). > - 첫 응답 120초 시계는 스트림이어도 같아요. 이 422 는 `x-should-retry: false` 로 와요 — 같은 요청을 자동으로 다시 보내지 마세요. stream: true 로 바꾸는 것도 새 생성이라 자동으로 하지 않아요. `Idempotency-Key` 를 실어 보낸 요청이면 받은 `x-gateway-status-url` 을 같은 API 키로 GET 조회하세요. `Idempotency-Key` 없이 보낸 요청엔 조회 주소가 안 오고 요청 ID 로 만든 주소로는 조회가 안 돼요 — 요청 ID 와 부른 시각을 admin@schoolorder.kr 로 알려 주세요. 요청 ID 는 머리글 `request-id`(같은 값이 `x-request-id`)와 오류 몸통 `error.request_id` 로 와요. 조회 주소는 `/v1/requests/` 꼴 상대 주소라 앞에 https://oneport.kr 를 붙여요. > - 고친 요청엔 새 Idempotency-Key 를 써요(같은 Idempotency-Key 로 본문을 바꾸면 409 `idempotency_conflict`). `Idempotency-Key` 를 실었으면 200 머리말을 받기 전의 연결 오류 · 시간 초과 뒤에 같은 Idempotency-Key · 같은 본문으로 다시 보내세요(몇 번 보내도 두 번 실행되지 않지만, 간격은 늘려 가며). 200 을 받은 뒤 끊긴 스트림은 잘린 답이라 같은 Idempotency-Key 는 409 예요 — 그때까지 쓴 토큰은 사용했고, 받은 글로 이어 받을 땐 새 Idempotency-Key 예요. > - 경계 사례는 이 장 전체 판(`https://oneport.kr/docs/reference/requests.full.md`)과 「에러」 장(`https://oneport.kr/docs/errors.md`)에 있어요. POST /v1/chat/completions · /v1/messages · /v1/responses에 Idempotency-Key를 보내면, 같은 API 키로 접수한 같은 요청을 다시 실행하지 않아요. 키를 보내지 않은 기존 호출에는 이 중복 방지 계약이 적용되지 않아요. **요청 헤더·경로** | 파라미터 | 받는 값 | 저희가 하는 일 | | --- | --- | --- | | `Idempotency-Key` | 공백 · 제어문자 없는 ASCII(영문 · 숫자 · 기호) 8~200자 | 위 세 생성 문에서 선택적으로 보내요. 같은 요청을 확인할 때는 그대로 유지하고, 다른 요청에는 새 값을 쓰세요. 같은 키로 본문·주소·벤더 버전 헤더를 바꾸면 409 idempotency_conflict예요. | | `Authorization 또는 x-api-key` (필수) | Bearer sk-so-… 또는 sk-so-… | 접수 때 사용한 API 키로 조회해요. Authorization에는 Bearer를 붙이고, x-api-key에는 키만 넣어요. 다른 키나 계정의 요청은 볼 수 없어요. 조회와 중복 방지는 그 API 키가 켜져 있을 때만 돼요 — 「새로 받기」로 바꾸면 접수가 새 API 키로 옮겨 가서, 옛 API 키로 받은 조회 주소를 새 API 키로 읽고 같은 Idempotency-Key 도 새 API 키에서 같은 접수로 묶여요. 누르는 순간 처리 중이던 요청도 끝이 적혀 새 API 키로 결과 · 정산을 읽어요(모델 회사로 보내기 전이었던 요청은 not_started 라 같은 Idempotency-Key 를 새 API 키로 다시 보내면 돼요). API 키를 폐기하거나 따로 만든 키로 바꾸면 옛 API 키로 접수한 요청은 새 API 키로 조회되지 않고(404), 새 API 키에선 같은 Idempotency-Key 도 새 요청이에요 — 폐기하기 전에 결과를 모르는 접수를 먼저 조회해 두세요. 여러 건을 묶어 돌리는 중엔 API 키를 바꾸지 마세요 — 「새로 받기」도 옛 API 키를 바로 꺼서 남은 호출이 401 이에요. 처리 중이던 접수는 「새로 받기」면 새 API 키로 이어지고(남은 건은 새 API 키를 넣고 같은 Idempotency-Key 로 보내면 돼요), 폐기하거나 따로 만든 키로 바꾸면 이어지지 않아요. 접수 기록은 저희가 지우지 않아요. | | `id` (필수) | 영속 요청 ID(request-id · x-request-id) | Idempotency-Key 로 접수한 요청의 x-gateway-status-url 이 가리키는 ID예요. x-gateway-request-id 와 같은 값이에요. 머리글 x-gateway-usage-id(사용 기록 ID)는 다른 값이라 조회 주소에 쓰지 마세요. 일반 요청 ID만으로 GET /v1/requests/{id}를 만들지 마세요. | openai SDK 로 실을 땐 Node 는 create(body, { headers: { "Idempotency-Key": key } }), Python 은 create(…, extra_headers={"Idempotency-Key": key}) 예요. 조회 주소는 Node 면 .withResponse() 가 준 response.headers.get("x-gateway-status-url"), Python 이면 with_raw_response 로 부른 응답의 headers 에서 읽어요(오류면 Node err.headers · Python e.response.headers). SDK 는 Idempotency-Key 를 스스로 싣지 않아요(SDK 동작). Go 의 net/http 는 Idempotency-Key 머리글이 있는 POST 를 다시 보내도 되는 요청으로 봐서, 다시 쓴 연결이 끊기면 스스로 한 번 더 보낼 수 있어요(Go 동작) — 같은 Idempotency-Key 라 두 번 실행되진 않지만 200 대신 409 가 올 수 있어요 — 그 409 를 조회해 completed 여도 답 본문은 다시 못 받아요(아래 response_stored 칸). ## 이미 접수한 요청 확인하기 같은 Idempotency-Key와 같은 본문을 다시 보내면 409 request_already_exists와 x-gateway-status-url을 받아요(같은 값이 Location · error.status_url 에도 와요). 새 생성이나 추가 차감을 하지 않고 기존 접수의 조회 주소를 알려드리는 응답이에요. 409 request_already_exists · idempotency_conflict 엔 x-should-retry: false 가 붙고 Retry-After 는 없어요 — SDK 는 다시 보내지 않아요. 다만 앞 접수가 실행 전에 막혀 끝났으면(execution not_started) 409 가 아니라 새 요청 ID 로 다시 접수돼 실행돼요. 처음 접수 응답을 못 받았다면 새 키를 만들지 말고 같은 키와 본문을 유지해서 주소를 확인하세요. 처음 응답이든 409 응답이든 x-gateway-status-url 을 GET으로 조회하세요. request-id · x-request-id 는 같은 영속 요청 ID를 유지해요. Idempotency-Key 없이 받은 일반 요청 ID는 상태 GET 대상이 아니에요. 오류 몸통의 error 안에는 type · message · request_id 가 오고, 갈래가 있으면 reason, 접수된 뒤의 실패면 status_url 이 더 와요(머리글 x-gateway-status-url 과 같은 값 · /v1/messages 는 바깥에 "type": "error" 와 request_id 가 더 있어요). 같은 키 · 같은 본문은 몇 번 다시 보내도 두 번 실행되지 않아요 — 앞 접수가 실행 전에 막혀 끝난(not_started) 게 아니면 처리 중이든 끝났든 실행 불명이든 409 이고, 막혀 끝났으면 그때 새로 접수돼요. 다시 보낼 땐 간격을 늘려 가며 보내요. 다시 접수되면 요청 ID 가 새로 생겨요. 그 뒤 같은 키에 오는 409 는 가장 새 요청 ID 의 조회 주소를 줘요. 옛 ID 도 계속 조회되지만 늘 not_started · not_charged 라서(이렇게 끝난 접수만 다시 접수돼요), 한 건의 요금은 가장 새 요청 ID 의 settlement 예요. 응답으로 실패를 받았으면 아래 표대로 해요. 키를 안 실은 호출은 예전 그대로예요 — 「에러」 장(https://oneport.kr/docs/errors.md)을 따라요. | 받은 응답 | 할 일 | Idempotency-Key | | --- | --- | --- | | 200(스트림이면 끝까지 받았을 때 — finish_reason · /v1/messages 는 message_stop · /v1/responses 는 response.completed) | 끝이에요. 그 호출에 쓴 몫은 usage.cost_credits 예요(스트림이면 끝 usage). 정산이 2초 안에 안 끝나면 그 칸 없이 와요 — 그땐 x-gateway-status-url 을 GET 해 settlement.cost_credits 를 봐요. | — | | 200 으로 시작한 스트림이 error 조각이나 끊김으로 끝남 | 잘린 답이에요 — 그때까지 쓴 몫은 이미 사용됐어요. 200 응답 머리글의 x-gateway-status-url 을 GET 해 결과와 정산을 확인해요. | 같은 키는 409 예요 — 다시 받으려면 새 키(새 호출이라 또 써요) | | 스트림 아닌 200 을 받았는데 몸통을 끝까지 못 읽음 | 답은 다 만들어졌고 쓴 몫도 사용됐어요. 머리글의 x-gateway-status-url 을 GET 해 결과(대개 completed)와 정산을 확인해요 — 이 머리글은 몸통보다 먼저 오니 몸통을 읽기 전에 저장해 두세요. | 같은 키는 409 예요 — 다시 받으려면 새 키(새 호출이라 또 써요) | | 409 request_already_exists | 그 응답의 x-gateway-status-url 을 GET 해요(처리 중 · 끝남 · 실행 여부 모름 모두 이 409 예요). | 같은 키 그대로 — 새 키로 바꾸지 않아요 | | 409 idempotency_conflict | 같은 키에 다른 본문 · 주소 · 버전 머리글을 보낸 거예요. | 고친 요청이면 새 키 · 같은 요청이면 처음 본문 그대로 | | 422 upstream_execution_unconfirmed | x-gateway-status-url 을 GET 해 결과와 정산을 확인해요. 자동으로 다시 만들지 않아요. | 다시 만들기로 사람이 정했을 때만 새 키(두 번 실행될 수 있어요) | | 상태 코드 없이 끊김(연결 오류 · 시간 초과) | 잠시 뒤 다시 보내요 — 저희에게 안 닿았거나 모델 회사로 보내기 전이었으면 새로 실행돼요. 모델 회사로 보낸 뒤 손님 쪽 연결이 끊기면(손님 코드의 시간 초과도 같아요) 저희도 그 호출을 멈추고 실행 불명으로 적어요 — 그땐 409 와 조회 주소가 오고, 조회하면 아래 표의 「그 밖의 started · execution unknown」 줄이라 사람이 정해요. 그래서 읽기 시간 제한은 300초로 두세요. 몇 번 보내도 두 번 실행되지 않으니 간격을 늘려 가며 보내요. | 같은 키 · 같은 본문 | | x-should-retry: false 없이 온 402 · 429 의 held_by_others · 429 rate_limit_error · 503 api_error · 529 · 그 밖의 5xx upstream_error | Retry-After 만큼(없으면 간격을 직접 정해) 기다렸다 다시 보내요. | 같은 키 · 같은 본문 | | 504 error.reason "timeout" · 조회 주소와 같이 온 503(request_state_unavailable · request_store_unavailable) · 조회 주소 없이 온 503(api_error · request_state_unavailable · request_store_unavailable) | 잠시 뒤 다시 보내요 — 같은 키라 두 번 실행되지 않아요(이미 접수돼 있으면 409 와 조회 주소가 와요). 조회 주소 없이 온 503 엔 Retry-After: 2 가 오고 x-should-retry 는 없어요. 504 와 조회 주소와 같이 온 503 은 x-should-retry: false 가 붙고 Retry-After 가 없어 SDK 는 다시 안 보내요 — 코드가 간격을 정해 한 번 보내요. | 같은 키 · 같은 본문 | | 402 below_minimum_balance · hold_exceeds_balance · 429 key_limit_exceeded(key_month_limit · key_day_limit) · group_limit_exceeded | 충전하거나 한도가 다시 열린 뒤 보내요(이 응답에도 조회 주소가 오고, 조회하면 not_started · not_charged 예요). | 같은 요청이면 같은 키 · 출력 상한을 줄여 고친 요청이면 새 키 | | 400 · 403 · 404 · 4xx upstream_error | 문장대로 고쳐요 — 그대로 보내면 같은 실패예요. | 고친 요청은 새 키 | | 401 authentication_error | API 키를 확인해요 — error.reason 이 이유를 말해요(「인증」 장 「키가 안 열릴 때」 표). 접수하는 사이 API 키가 꺼졌거나 기한이 끝났거나 지갑이 멈춘 것도 이 401 이에요. 접수 전이라 조회 주소가 없어요. | 접수 전이라 같은 Idempotency-Key 를 그대로 써도 돼요 — 「새로 받기」로 받은 새 API 키로 보내도 앞 접수와 이어지고, 폐기한 뒤 따로 만든 API 키에선 새 요청이에요 | | x-should-retry: false 와 같이 온 429 rate_limit_error | 모델 회사가 실행 전에 거절했는데 저희가 접수를 풀지 못한 거예요(Retry-After 없음). x-gateway-status-url 을 GET 해 execution 을 봐요. 조회 응답엔 처음 실패의 type 이 안 와요 — 이 줄로 받았다는 걸 조회 주소와 같이 적어 두세요. | not_started 면 같은 키 · 같은 본문 · started 면 새 키 — 실행 전 거절이라 두 번 실행되지 않아요(아래 조회 표의 「그 밖의 started」 줄보다 앞서요) | | 그 밖에 x-should-retry: false 와 조회 주소가 같이 온 실패 | 자동으로 다시 보내지 말고 x-gateway-status-url 을 GET 해 execution 을 봐요(아래 표). | not_started 면 같은 키 · 같은 본문으로 다시 받아요 · 그 밖엔 같은 키는 409 예요 — 새 키는 아래 「조회 결과로 다음 할 일 고르기」 표대로 | **처음 요청에 사용한 API 키와 받은 요청 ID로 조회** ```bash curl "https://oneport.kr/v1/requests/$REQUEST_ID" \ -H "Authorization: Bearer $ONEPORT_API_KEY" ``` **스트림이 아직 흐르는 중인 응답 예시(처음 응답 200 · 결과 · 정산 미확인)** ```json { "id": "00000000-0000-4000-8000-000000000001", "object": "gateway.request", "execution": "started", "http_status": 200, "created_at": "2026-10-28T00:00:00.123456+00:00", "updated_at": "2026-10-28T00:00:01.5+00:00", "result": { "state": "unknown" }, "settlement": { "state": "unknown", "cost_credits": null }, "response_stored": false } ``` **실행 전에 막혀 끝난 접수(같은 키로 다시 보내면 새로 접수돼요)** ```json { "id": "00000000-0000-4000-8000-000000000002", "object": "gateway.request", "execution": "not_started", "http_status": 429, "created_at": "2026-10-29T00:00:00.654321+00:00", "updated_at": "2026-10-29T00:00:00.98765+00:00", "result": { "state": "failed" }, "settlement": { "state": "not_charged", "cost_credits": 0 }, "response_stored": false } ``` **실행 불명 422 를 받은 접수(정산이 not_charged 여도 실행됐을 수 있어요 — 자동으로 다시 만들지 않아요)** ```json { "id": "00000000-0000-4000-8000-000000000003", "object": "gateway.request", "execution": "unknown", "http_status": 422, "created_at": "2026-10-29T00:00:00.112233+00:00", "updated_at": "2026-10-29T00:02:00.445566+00:00", "result": { "state": "unknown" }, "settlement": { "state": "not_charged", "cost_credits": 0 }, "response_stored": false } ``` | 응답 | 뜻 | | --- | --- | | execution | 접수·전송 관측 상태예요 — claimed(접수만) · started(모델 회사로 보냄) · not_started(실행 전에 막힘 — 모델 회사로 안 보냈거나 보낸 시도가 전부 실행 전 거절로 확인됨 · result.state 는 failed · settlement.state 는 not_charged · 같은 키와 본문으로 다시 보내면 새 요청 ID 로 다시 접수돼요) · unknown(보냈는지 모름 — 모델 회사로 보내기 시작한 뒤 결과를 못 적은 것이라 실행됐을 수 있어요 · 같은 키로는 다시 받지 않아요). 보내기 전에 저희 쪽에서 실패한 접수는 unknown 이 아니라 not_started 로 적혀요(2026-10-29 부터). started나 HTTP 200만으로 답변 완료를 판단하지 마세요. 글 문은 접수 뒤 약 14분 안에 끝나서, 접수 뒤 약 14분이 지나도 claimed 면 모델 회사로 보내지 않은 채 멈춘 접수예요. 접수 뒤 15분(900초)이 지나면 조회가 execution not_started · result.state failed · settlement.state not_charged 로 바뀌고, 같은 키 · 같은 본문으로 다시 보내면 새로 접수돼 실행돼요. 그 전에 같은 키로 보내면 409 request_already_exists 예요 — 새 키로 바꾸지 말고 15분이 지난 뒤 같은 키로 보내요. 2026-10-29 전에 접수된 요청은 보낸 뒤 실행 전에 막힌 것도 started 로 남아 있어요 — http_status 가 402 · 429 · 503 · 504 · 529 면 모델 회사가 실행하지 않은 거예요. 그 뒤에 접수된 요청은 not_started 로 적혀서 이 조합이 새로 생기지 않아요(created_at 날짜로 가르지 말고 이 조합으로 봐요). 같은 키는 409 라서 다시 받으려면 새 키로 보내요 — 앞 요청이 실행되지 않았으니 새 키여도 두 번 실행되는 게 아니에요. | | http_status | 저희가 그 요청에 돌려준 HTTP 상태예요(스트림이면 시작할 때의 200). null 이면 아직 기록된 응답 상태가 없는 거예요 — 대개 처리 중이지만, 저희가 상태를 못 적은 실패(503 request_state_unavailable 등)도 null 로 남아요. 글 문은 접수(created_at) 뒤 약 14분 안에 끝나서, 약 14분이 지나면 http_status 는 더 바뀌지 않아요(그때도 null 이면 execution 으로 판단해요). execution · result · settlement 도 그때 정해지는데, claimed 로 멈춘 접수만 15분이 지나 not_started · failed · not_charged 로 바뀌어요. | | result.state | completed는 완료 확인, failed는 확인된 실패, partial은 부분 결과, unknown은 결과 미확인이에요. 접수 뒤 약 14분이 지나도 unknown 이면 최종이에요(422 upstream_execution_unconfirmed 가 이래요 — 저희가 모델 회사 응답을 다 못 읽었거나 보낸 뒤 끊긴 경우예요) — 다시 조회해도 안 바뀌어요. execution 이 claimed 일 때만 빼고요(위 execution 칸). 422 를 받은 뒤의 조회는 처음부터 최종이라 14분을 기다리지 않아도 돼요 — 저희가 결과를 적은 뒤에 422 를 보내요. | | settlement.state | settled는 고객 정산 확인, not_charged는 요금이 안 붙은 것으로 확인, unknown은 정산 미확인이에요. 결과와 정산은 따로 확인해요. 접수 뒤 약 14분이 지나도 unknown 이면 저절로 안 바뀌어요 — 요청 ID 와 부른 시각을 admin@schoolorder.kr 로 알려 주세요. | | settlement.cost_credits | 확인된 차감 크레딧이에요 — 처음 응답 usage.cost_credits 와 같은 이름 · 같은 자이고 둘 다 JSON 숫자예요. settled 면 그 값, not_charged 면 0 이에요. 정산이 unknown이면 null이며, 0으로 바꿔 읽지 마세요. 호출 하나하나의 내역을 내려받는 API 는 없어요 — 건별로 맞춰 보실 땐 저장해 둔 조회 주소를 GET 하고, 날짜별 합은 정산 원장 CSV(「크레딧」 장)예요. 호출마다 한 줄인 파일은 대시보드 「감사 로그 받기」인데 요청 ID 칸은 없어요(칸은 「Anthropic·OpenAI 콘솔에서 옮겨 오기」 장 「사용량 보고」). | | response_stored | 항상 false예요. 요청·답변 원문을 보관하지 않으므로 이 조회에서 답변 본문을 다시 받을 수는 없어요. | ## 조회 결과로 다음 할 일 고르기 | 조회 결과 | 뜻 · 할 일 | Idempotency-Key | | --- | --- | --- | | claimed | 접수만 됐어요 — 기다렸다 다시 조회해요. 접수 뒤 15분이 지나면 not_started 로 바뀌어요(위 execution 칸). | 15분이 지나 not_started 가 되면 같은 키 | | not_started | 실행 전에 막혔어요 — 요금이 없어요. | 같은 키 · 같은 본문으로 다시 보내요 | | started · result.state completed | 답이 끝까지 만들어졌어요 — 쓴 몫은 settlement.cost_credits 예요. 답 본문은 다시 못 받아요. | 같은 키는 409 예요 — 다시 받으려면 새 키(새로 실행되고 요금도 붙어요) | | started · http_status 402 · 429 · 503 · 504 · 529 | 2026-10-29 전에 접수된 요청만 이래요 — 실행되지 않았어요(위 execution 칸). | 같은 키는 409 라 새 키 — 두 번 실행되지 않아요 | | started · result.state failed · http_status 400~499(위 줄의 402 · 429 말고) | 모델 회사가 요청을 거절했어요 — 요금은 settlement 대로예요(대개 not_charged). | 문장대로 고친 요청은 새 키 | | 그 밖의 started(result.state partial · unknown 등) · execution unknown | 실행됐을 수 있어요 — 자동으로 다시 만들지 않아요. 쓴 몫은 settlement 로 따로 봐요. | 새 키는 사람이 다시 만들기로 정했을 때만(두 번 실행될 수 있어요) | > **⚠️ 처리 불명은 자동으로 다시 생성하지 마세요** > > 생성 요청의 422 upstream_execution_unconfirmed 는 실행 결과를 확인하지 못했다는 뜻이에요(x-should-retry: false 는 held_by_others 밖의 한도 429 · 4xx upstream_error · 504 timeout 같은 다른 실패에도 붙으니, 그 머리글만으로 실행 불명이라 읽지 말고 type 으로 가르세요). 이 조회 문은 추가 생성·과금을 하지 않아요. 결과가 unknown인데 정산이 not_charged일 수도 있으며, 요금이 안 붙었다는 것만으로 요청이 실행되지 않았다고 판단하면 안 돼요. 이땐 저희도 실행 여부를 몰라요. 같은 키는 409 라서, 다시 만들려면 사람이 정해서 새 키로 보내요(새 요금이 붙을 수 있어요). 조회 문은 분당 호출 수를 막지 않아요. 처리 중이면 몇 초 간격으로 GET 하고, 위 http_status · result.state 칸대로 값이 정해지면 멈춰요. 조회 주소는 늘 마지막에 받은 x-gateway-status-url 로 덮어써 두세요. created_at · updated_at 은 UTC ISO 8601 이에요 — 소수 초가 붙을 수 있고(자릿수는 그때마다 달라요) 끝은 +00:00 이라, 글자로 견주지 말고 시각으로 읽으세요. Python 은 3.11 이상이면 datetime.fromisoformat 으로 읽혀요(3.10 이하는 소수 자릿수가 3 · 6 이 아니면 ValueError 라 dateutil.parser.isoparse 를 쓰세요). Node 는 new Date(…), Go 는 time.Parse(time.RFC3339, …) 로 읽혀요(언어 동작). 조회 오류는 키가 없거나 유효하지 않으면 401, 그 키의 요청을 찾지 못하면 404, 상태를 읽지 못하면 503 request_state_unavailable 또는 request_store_unavailable예요. API 키를 잠깐 못 읽으면 503 api_error 도 와요. 이 503 셋엔 Retry-After: 2 가 오고 x-should-retry 는 없어요 — 요청 ID를 유지하고 2초 뒤 같은 주소로 다시 GET 하세요. 이 주소는 생성·취소·답변 다운로드를 지원하지 않아요.