AUTH_REQUIREDHTTP 401수정 후 재요청- 의미
- 인증 정보가 없거나 유효하지 않습니다.
- 고객 조치
- Bearer API 키 또는 로그인 세션을 확인하세요.
- 재시도
- 인증 정보를 고친 뒤 다시 요청하세요.
개발자 문서
https://hankiro.com/api/v1https://hankiro.com/v1npm 공개 준비 중. 공개 전에는 설치 문구를 표시하지 않습니다.
LLM 모델 ID 전체 3개 중 현재 3개를 호출할 수 있습니다. 미디어 실행 ID 전체 26개 중 현재 22개에 실행 화면이 제공됩니다. 실제 연결 상태는 모델 화면에서 확인하세요.
Step 1
https://hankiro.com/keys에서 키를 발급합니다. 전체 키 값은 발급 직후 한 번만 보이며, 미디어·LLM·지갑·사용량 모두 이 키 하나로 부릅니다. 실제 키 값을 문서나 저장소에 적지 마세요.
Authorization: Bearer hk_live_...
키는 발급 뒤에도 이름·허용 IP·허용 모델·분당 한도를 바꿀 수 있습니다(PATCH /api/v1/keys/{id}). 키 값 자체는 바뀌지 않습니다.
Step 2
미디어는 POST /api/v1/run/ 뒤에 정확한 모델 ID를 붙여 { "input": {...} }를 보냅니다. LLM은 OpenAI Chat Completions 형식 그대로입니다. 아래 예제는 지금 호출 가능한 모델로 서버가 고른 것입니다. Idempotency-Key는 한 실행을 대표하는 UUID를 새로 만들어 보내면 같은 키의 재요청이 같은 결과로 재생됩니다.
curl 예제의 $(uuidgen)은 bash/zsh용입니다. Windows cmd/PowerShell에서는 그대로 붙여넣으면 문자열 $(uuidgen)이 키로 전송되어 모든 요청이 같은 키로 재생되므로, UUID를 직접 만들어 넣으세요.
google/veo3.1/text-to-videocurl -X POST https://hankiro.com/api/v1/run/google/veo3.1/text-to-video \
-H "Authorization: Bearer hk_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"input":{"prompt":"비 온 뒤 서울 골목을 천천히 따라가는 영화 같은 카메라 움직임","aspect_ratio":"16:9","duration":8,"resolution":"1080p"}}'import requests
import uuid
res = requests.post(
"https://hankiro.com/api/v1/run/google/veo3.1/text-to-video",
headers={
"Authorization": "Bearer hk_live_...",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"input": {
"prompt": "비 온 뒤 서울 골목을 천천히 따라가는 영화 같은 카메라 움직임",
"aspect_ratio": "16:9",
"duration": 8,
"resolution": "1080p"
}
},
)
print(res.json())const res = await fetch("https://hankiro.com/api/v1/run/google/veo3.1/text-to-video", {
method: "POST",
headers: {
Authorization: "Bearer hk_live_...",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"input": {
"prompt": "비 온 뒤 서울 골목을 천천히 따라가는 영화 같은 카메라 움직임",
"aspect_ratio": "16:9",
"duration": 8,
"resolution": "1080p"
}
}),
});
console.log(await res.json());응답은 task_uuid와 status입니다(접수 중이면 processing·202, 즉시 완료면 completed·200). 3단계에서 결과를 받습니다.
gemini-3.1-flash-litecurl -X POST "https://hankiro.com/v1/chat/completions" \
-H "Authorization: Bearer hk_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-3.1-flash-lite",
"messages": [
{
"role": "user",
"content": "안녕하세요"
}
],
"max_tokens": 65536
}'import requests
import uuid
res = requests.post(
"https://hankiro.com/v1/chat/completions",
headers={
"Authorization": "Bearer hk_live_...",
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
json={
"model": "gemini-3.1-flash-lite",
"messages": [
{
"role": "user",
"content": "안녕하세요"
}
],
"max_tokens": 65536
},
)
print(res.json())const res = await fetch("https://hankiro.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: "Bearer hk_live_...",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
"model": "gemini-3.1-flash-lite",
"messages": [
{
"role": "user",
"content": "안녕하세요"
}
],
"max_tokens": 65536
}),
});
console.log(await res.json());응답은 choices[0].message.content와 usage.amount_krw(실제 정산 원화)를 담아 한 번에 반환됩니다. 모델 목록은 GET https://hankiro.com/v1/models를 HTTP로 직접 읽습니다(응답은 { total, items } — OpenAI SDK의 models.list()는 미지원).
Step 3
미디어 생성은 수분이 걸릴 수 있습니다. 작업 상태 조회 GET /api/v1/run/tasks/{task_uuid}를 queued·processing 동안 지수 백오프로 반복하세요. 완료되면 결과 파일의 private signed URL이 output.video.url · output.audio.url · output.images[].url에 옵니다(output.url은 없습니다). 주소는 함께 오는 url_expires_in초(1시간) 뒤 만료되므로 파일을 바로 내려받고, URL 대신 task_uuid를 보관했다가 다시 조회해 새 주소를 받으세요.
const BASE = "https://hankiro.com/api/v1";
const headers = { Authorization: "Bearer hk_live_..." };
async function waitForTask(taskUuid) {
let delay = 2000;
for (;;) {
const res = await fetch(`${BASE}/run/tasks/${taskUuid}`, { headers });
const task = await res.json();
if (task.status === "completed") return task.output; // signed URL은 1시간 뒤 만료
if (task.status === "failed") throw new Error(task.error?.code || "TASK_FAILED");
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.5, 15000);
}
}402 INSUFFICIENT_FUNDS선불 잔액 부족. required_krw·shortfall_krw가 함께 옵니다. 충전 전에는 같은 요청을 반복하지 마세요.
429 RATE_LIMITED키의 분당 한도 초과. Retry-After(초) 뒤에 같은 요청을 다시 보내세요.
502 UPSTREAM_INDETERMINATE공급사 접수 여부 불명. 새 요청을 만들지 말고 task_uuid 조회 또는 같은 Idempotency-Key로 상태를 확인하세요.
SDK의 base_url을 https://hankiro.com/v1로 바꾸면 그대로 동작합니다. 현재 비스트리밍 텍스트 응답만 지원합니다. Idempotency-Key가 없으면 서버가 요청마다 새 UUID를 만들어 쓰므로 재생 보호는 헤더를 보낸 요청에만 적용됩니다.
OpenAI SDK는 기본 2회 자동 재시도(408·409·429·5xx·연결 오류)합니다. Idempotency-Key 없이 쓰면 재시도가 각각 별도 예약·과금이 될 수 있으니, 아래처럼 호출마다 UUID를 헤더로 고정하거나 max_retries=0으로 두세요. SDK가 붙이는 재시도 표식(x-stainless-retry-count > 0)이 있는데 Idempotency-Key가 없으면 서버는 중복 과금을 막기 위해 그 재시도를 400 INVALID_INPUT으로 거절합니다(첫 요청은 그대로 처리됩니다).
from openai import OpenAI
import uuid
client = OpenAI(base_url="https://hankiro.com/v1", api_key="hk_live_...")
# SDK 기본 자동 재시도(2회)가 별도 예약·과금이 되지 않도록 호출마다 Idempotency-Key를 고정합니다.
# 재시도 자체를 끄려면 OpenAI(..., max_retries=0)
res = client.chat.completions.create(
model="gemini-3.1-flash-lite",
messages=[{"role": "user", "content": "안녕하세요"}],
max_tokens=65536,
extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)
print(res.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://hankiro.com/v1", apiKey: "hk_live_..." });
// SDK 기본 자동 재시도(2회)가 별도 예약·과금이 되지 않도록 호출마다 Idempotency-Key를 고정합니다.
// 재시도 자체를 끄려면 new OpenAI({ ..., maxRetries: 0 })
const res = await client.chat.completions.create(
{
model: "gemini-3.1-flash-lite",
messages: [{ role: "user", content: "안녕하세요" }],
max_tokens: 65536,
},
{ headers: { "Idempotency-Key": crypto.randomUUID() } },
);
console.log(res.choices[0].message.content);| 처리 | 옵션 | 결과 |
|---|---|---|
| 무시 | temperature, top_p, presence_penalty, frequency_penalty, stop, user, seed, logit_bias, logprobs, top_logprobs, store, metadata, service_tier, parallel_tool_calls, prompt_cache_key, safety_identifier, n=1, response_format.type=text, 그 외 목록에 없는 키 | 200 · 응답 헤더 X-Hankiro-Ignored에 무시한 옵션 이름(값이 null이면 없는 것으로 취급) |
| 거절 | stream=true, n>1, response_format(json_schema 등), stream_options, tools, functions, tool_choice, function_call, modalities, audio, prediction, web_search_options, thinking, enable_thinking, thinking_config, reasoning_effort | 400 INVALID_INPUT · "HANKIRO 게이트웨이는 현재 비스트리밍 텍스트 응답만 지원합니다" |
| 거절 | max_tokens와 max_completion_tokens를 서로 다른 값으로 동시 지정 | 400 INVALID_INPUT · 하나만 보내세요(같은 값이면 통과) |
이미지 입력은 content 배열의 image_url에 PNG·JPEG·WebP data URL만 받습니다(원격 주소 불가). 요청당 최대 4장, 장당 2MB, 합계 2.5MB. 한도를 넘는 첨부는 호출·차감 전에 400 INVALID_INPUT으로 거절됩니다.
POST /api/v1/quote인증 없이, 예약 없이 판매가만 계산합니다. 실행과 같은 입력 검증을 거치므로 400 INVALID_INPUT이면 실행도 실패합니다. 응답의 amount_krw는 최대 예약액(판매가, 부가세 포함)이고 unit에 단위별 판매가가 옵니다. 웹 크레딧 표시는 이 원화 값을 환산하며, API 필드와 요청 상한의 단위는 기존 KRW를 유지합니다.
{
"model_id": "google/veo3.1/text-to-video",
"input": {
"prompt": "비 온 뒤 서울 골목을 천천히 따라가는 영화 같은 카메라 움직임",
"aspect_ratio": "16:9",
"duration": 8,
"resolution": "1080p"
}
}{
"model_id": "gemini-3.1-flash-lite",
"messages": [
{
"role": "user",
"content": "안녕하세요"
}
],
"max_tokens": 256
}웹에서는 예상 사용량을 크레딧으로 표시합니다. 실행 직전 예약 후 결과가 확정되면 실제 사용량으로 정산되고, 공급사가 호출 전에 명시적으로 거절한 요청은 예약이 해제됩니다. 클라이언트가 작업 조회를 멈춰도 접수 번호가 있는 작업과 저장된 결과는 백그라운드에서 계속 확인합니다.
네트워크 단절처럼 공급사 접수 여부를 확정할 수 없는 경우에는 중복 실행이나 잘못된 환불을 막기 위해 예약을 즉시 해제하지 않습니다. UPSTREAM_INDETERMINATE가 오면 새 요청을 만들지 말고, 기존 task_uuid를 조회하거나 같은 본문과 같은 Idempotency-Key로 상태를 확인하세요.
지갑 GET /api/v1/wallet과 사용량 GET /api/v1/usage·/api/v1/usage/export(CSV)는 로그인 세션뿐 아니라 Bearer 키로도 읽을 수 있습니다(읽기 전용, Cache-Control: private, no-store). CSV는 기본으로 기존 KRW 필드를 유지하며, ?display=credits를 붙이면 크레딧 열로 내려받습니다. 웹 사용 내역의 CSV 버튼은 이 크레딧 형식을 사용합니다.
키에 호출 서버의 정확한 IPv4 또는 IPv6 주소를 최대 16개 등록할 수 있습니다. 비워두면 모든 IP에서 호출할 수 있으며, CIDR 대역 표기는 지원하지 않습니다. 허용 목록과 호출 주소가 다르면 403 IP_NOT_ALLOWED, 신뢰할 수 있는 호출자 IP를 확인할 수 없으면 503 CLIENT_IP_UNAVAILABLE을 반환합니다.
HANKIRO 앞에 별도 리버스 프록시나 CDN을 두면 실제 호출자 대신 프록시의 외부 송신 IP가 확인될 수 있습니다. 원본 IP 전달을 신뢰할 수 없는 구성에서는 네트워크 경로와 실제로 식별되는 IP를 먼저 확인한 뒤 제한을 설정하세요.
키에 설정한 분당 한도는 그 키로 인증된 모든 Bearer 요청에 적용됩니다. 실행 요청뿐 아니라 입력 오류, 같은 멱등 키의 재요청, 충돌 응답, 작업 상태 조회, 지갑·사용량 조회도 각각 한 번의 요청으로 계산합니다. 로그인 세션으로 보낸 요청은 API 키 한도에 포함되지 않습니다. 한도를 넘으면 429 RATE_LIMITED와 Retry-After 헤더가 반환됩니다. 한도를 0으로 설정하면 분당 제한 없이 요청 수만 기록합니다(1~60,000).
발급 뒤 정책 변경은 PATCH /api/v1/keys/{id}에 { name?, allowIps?, allowModels?, rpm? }를 보냅니다(로그인 세션 전용, 소유자만). 허용 모델을 지정하면 그 외 모델은 403 MODEL_FORBIDDEN으로 거절됩니다.
키 페이지에서 최근 30일 동안의 키 발급·폐기, IP 차단, 호출자 IP 확인 실패, 요청 한도 초과를 확인할 수 있습니다. 같은 키에서 같은 종류로 발생한 활동은 분 단위로 묶어 횟수와 함께 표시하며, 호출자 IP는 전체 주소 대신 일부만 마스킹해 보여줍니다.
Error contract
실패 응답은 항상 같은 error 봉투를 사용하며, 오류별 복구 정보가 최상위 필드로 추가될 수 있습니다. 분기에는 번역될 수 있는 message가 아니라 안정적인 error.code를 사용하세요.
{
"error": {
"code": "MODEL_NOT_CONNECTED",
"message": "이 모델은 카탈로그에 있지만 현재 호출 연결 대상이 아닙니다."
}
}402 INSUFFICIENT_FUNDS에는 currency, available_krw, required_krw, shortfall_krw가 함께 반환됩니다. 잔액 정보가 담긴 응답은 Cache-Control: private, no-store로 전달됩니다.
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "사용 가능한 선불 원화 잔액이 부족합니다."
},
"currency": "KRW",
"available_krw": 1200,
"required_krw": 1800,
"shortfall_krw": 600
}REQUEST_IN_PROGRESS가 오면 Retry-After 헤더만큼 기다린 뒤, 원래 본문과 같은 Idempotency-Key로 다시 요청하세요. 새 키를 만들면 같은 작업이 중복 실행될 수 있습니다.
AUTH_REQUIREDHTTP 401수정 후 재요청FORBIDDENHTTP 403재시도 안 함MODEL_FORBIDDENHTTP 403재시도 안 함IP_NOT_ALLOWEDHTTP 403재시도 안 함CLIENT_IP_UNAVAILABLEHTTP 503백오프MODEL_NOT_FOUNDHTTP 404수정 후 재요청TASK_NOT_FOUNDHTTP 404재시도 안 함NOT_FOUNDHTTP 404수정 후 재요청INVALID_JSONHTTP 400수정 후 재요청INVALID_INPUTHTTP 400수정 후 재요청IDEMPOTENCY_KEY_REQUIREDHTTP 400수정 후 재요청INVALID_IDEMPOTENCY_KEYHTTP 400수정 후 재요청IDEMPOTENCY_KEY_REUSEDHTTP 409수정 후 재요청REQUEST_IN_PROGRESSHTTP 409같은 멱등 키INSUFFICIENT_FUNDSHTTP 402재시도 안 함RESERVATION_FAILEDHTTP 503백오프DISPATCH_NOT_STARTEDHTTP 503백오프MODEL_NOT_CONNECTEDHTTP 503재시도 안 함PROVIDER_NOT_CONFIGUREDHTTP 503백오프UPSTREAM_FAILEDHTTP 502백오프UPSTREAM_REJECTEDHTTP 422수정 후 재요청UPSTREAM_TIMEOUTHTTP 504같은 멱등 키UPSTREAM_INDETERMINATEHTTP 502상태 조회ARTIFACT_STORAGE_FAILEDHTTP 502같은 멱등 키KEY_COUNTER_FAILEDHTTP 503백오프TASK_STATE_MISSINGHTTP 500상태 조회REPLAY_STATE_INVALIDHTTP 500재시도 안 함TASK_FAILEDHTTP 200재시도 안 함INVALID_USAGE_RANGEHTTP 400수정 후 재요청INVALID_USAGE_CURSORHTTP 400수정 후 재요청INVALID_USAGE_FILTERHTTP 400수정 후 재요청EXPORT_TOO_LARGEHTTP 422수정 후 재요청SERVICE_UNAVAILABLEHTTP 503백오프CONFLICTHTTP 409수정 후 재요청PAYLOAD_TOO_LARGEHTTP 413수정 후 재요청RATE_LIMITEDHTTP 429백오프INTERNAL_ERRORHTTP 500백오프