> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apiyi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 로그 쿼리 API

> 각 요청에 대해 모델, 실제 과금, 지연 시간, 오류 코드를 가져오기 위해 호출 로그를 프로그래밍 방식으로 조회하여, 자동화된 대사와 셀프서비스 문제 해결을 가능하게 합니다

## API 개요

Log Query API는 계정에서 이루어진 **모든 API 호출**에 대한 상세 기록을 반환합니다.
여기에는 사용된 모델, 실제 과금 금액, 지연 시간, 호출이 스트리밍으로 전송되었는지 여부,
그리고 호출이 실패했을 때의 오류 코드가 포함됩니다.

이 API는 [Balance Query API](/ko/api-capabilities/balance-query)를 보완합니다. 잔액 조회는 남은 크레딧이 얼마인지 알려주고, 로그 조회는 그 크레딧이 어디에 사용되었는지 알려줍니다.

대표적인 사용 사례 3가지입니다:

<CardGroup cols={3}>
  <Card title="자동 대사" icon="calculator">
    시간 범위별 또는 모델별로 실제 지출을 집계하여 자체 과금 내역과 대사합니다
  </Card>

  <Card title="셀프서비스 문제 해결" icon="bug">
    실패한 요청의 오류 코드를 확인하여 매개변수 문제와 상위 시스템 문제를 구분합니다
  </Card>

  <Card title="지원 티켓" icon="life-buoy">
    지원팀에 `request_id`를 제공하면 정확한 호출을 특정할 수 있습니다
  </Card>
</CardGroup>

<Info>
  로그는 콘솔의 Logs 페이지에서도 볼 수 있습니다. 이 API는 동일한 데이터에 대한 프로그램 방식의 진입점으로, 자동 대사, 예약된 내보내기 또는 자체 모니터링에 데이터를 공급하는 용도로 사용됩니다. 수동 확인은 콘솔을 사용하십시오 — [내 호출 기록을 보는 방법](/ko/faq/call-logs)을 참조하십시오.
</Info>

## 시스템 Token을 얻는 방법

로그 조회 API는 **시스템 Token**으로 인증하며, 이는 API key와는 다른 개념입니다(이 페이지 끝의 중요 참고 사항을 참조합니다).

<Steps>
  <Step title="콘솔에 접근">
    프로필 페이지에 접근하려면 `api.apiyi.com/account/profile`를 방문합니다
  </Step>

  <Step title="System Token 찾기">
    페이지 하단에서 “Account Options - System Token” 섹션을 찾습니다
  </Step>

  <Step title="Generate AccessToken">
    이후 API 질의에 사용할 수 있는 AccessToken을 받으려면 계정 비밀번호를 입력합니다
  </Step>
</Steps>

<img src="https://mintcdn.com/apiyillc/PXVoab-l7wSQlQVE/images/apiyi-system-accesstoken.png?fit=max&auto=format&n=PXVoab-l7wSQlQVE&q=85&s=eb4f48476a795dfa5bfd7cb053081bdc" alt="System Token 받기" width="1020" height="460" data-path="images/apiyi-system-accesstoken.png" />

## API 정보

| 항목          | 설명                                                  |
| ----------- | --------------------------------------------------- |
| **API URL** | `https://api.apiyi.com/api/log/self`                |
| **메서드**     | `GET`                                               |
| **인증**      | Authorization 헤더(원시 token 문자열, **`Bearer` 접두사 없음**) |
| **응답 형식**   | JSON(gzip 압축)                                       |
| **데이터 범위**  | 본인 계정의 로그만                                          |

## 요청 세부 정보

### 요청 헤더

| 헤더 이름           | 필수  | 설명                                |
| --------------- | --- | --------------------------------- |
| `Authorization` | 예   | 시스템 token입니다. 원시 token 문자열로 전달합니다 |
| `Accept`        | 아니오 | 권장: `application/json`            |

### 쿼리 파라미터

| 파라미터              | 유형  | 필수  | 설명                                         |
| ----------------- | --- | --- | ------------------------------------------ |
| `p`               | 정수  | 아니오 | 페이지 번호입니다. **0부터 시작**합니다(1이 아님)            |
| `page_size`       | 정수  | 아니오 | 페이지당 레코드 수입니다. **최대 10개로 제한**됩니다(아래 경고 참고) |
| `type`            | 정수  | 아니오 | 로그 유형입니다. 정산에는 `2`를 전달하십시오. 아래 표를 참조하십시오   |
| `model_name`      | 문자열 | 아니오 | 모델로 정확히 일치하는 필터입니다. 예: `gpt-5.6`           |
| `token_name`      | 문자열 | 아니오 | token 이름으로 필터링합니다                          |
| `start_timestamp` | 정수  | 아니오 | 시작 시간입니다. Unix 초 단위입니다                     |
| `end_timestamp`   | 정수  | 아니오 | 종료 시간입니다. Unix 초 단위입니다                     |
| `group`           | 문자열 | 아니오 | 그룹으로 필터링합니다                                |

<Warning>
  **`page_size`의 실제 상한은 10입니다.** `page_size=100`를 전달해도 10개 레코드만 반환됩니다 —
  이는 오류가 아니며, “이 기간에 호출을 10번만 했습니다”라고 오해하기 쉽습니다.
  **의미 있는 시간 범위는 모두 페이지네이션이 필요합니다.** 응답이 빈 배열을 반환할 때까지 반복해야 합니다.
  아래의 Python 및 Node.js 예제는 이미 이를 처리합니다.
</Warning>

### 로그 유형

| 값   | 의미     | 비고                                        |
| --- | ------ | ----------------------------------------- |
| `1` | 충전     | 충전 전후의 잔액을 기록합니다. `quota`은 0입니다           |
| `2` | **소비** | **정산에 필요한 유일한 유형입니다**. `quota`는 실제 과금액입니다 |
| `3` | 관리     | 계정 변경 및 유사한 작업입니다. `quota`은 0입니다          |
| `4` | 시스템    | 시스템에서 부여한 크레딧 등입니다. `quota`은 0입니다         |

<Warning>
  **지출을 계산할 때는 항상 `type=2`를 전달하십시오.** 그렇지 않으면 충전 및 시스템 부여 레코드도 함께 반환됩니다.
  이들의 `quota`은 0이지만, `model_name`와 `token_name`도 비어 있으므로, 단순 합산하거나 모델별로 그룹화하면 잘못된 결과가 나옵니다.
</Warning>

## 응답 세부 정보

### 성공 응답 예시

```json theme={null}
{
  "success": true,
  "message": "",
  "data": [
    {
      "request_id": "2026080114481936471351696e93ae3FTV8WKfk",
      "created_at": 1785595715,
      "type": 2,
      "content": "Fixed model price 0.015, group ratio 1",
      "username": "your-account",
      "token_name": "production-primary",
      "token_group": "default",
      "model_name": "gpt-5.6",
      "quota": 7500,
      "prompt_tokens": 1000,
      "completion_tokens": 0,
      "duration_for_view": 16,
      "is_stream": false,
      "error_code": "",
      "other": "{\"billing_type\":\"by_count\",\"request_path\":\"/v1/images/generations\",\"group_ratio\":1,\"model_ratio\":1,\"usage\":{}}"
    }
  ]
}
```

### 핵심 응답 필드

| 필드명                                   | 유형  | 설명                                                    |
| ------------------------------------- | --- | ----------------------------------------------------- |
| `quota`                               | 정수  | **이 호출에 대해 실제로 청구된 금액**이며, 크레딧 단위입니다; ÷ 500,000 = USD |
| `content`                             | 문자열 | 사람이 읽을 수 있는 과금 메모, 예: 고정 모델 가격 및 그룹 요율 배수             |
| `model_name`                          | 문자열 | 실제로 과금된 모델                                            |
| `token_name`                          | 문자열 | 어떤 API 키가 호출했는지                                       |
| `token_group`                         | 문자열 | token의 그룹 — 이것이 그룹 식별자임에 유의하십시오. FAQ를 참조하십시오          |
| `prompt_tokens` / `completion_tokens` | 정수  | 입력 / 출력 token 수                                       |
| `duration_for_view`                   | 정수  | 호출 지속 시간(초)                                           |
| `is_stream`                           | 불리언 | 호출이 스트리밍되었는지 여부                                       |
| `error_code`                          | 문자열 | 실패 사유 코드; 성공 시에는 빈 문자열                                |
| `created_at`                          | 정수  | 호출 시각, Unix 초                                         |
| `request_id`                          | 문자열 | **요청 ID — 지원 티켓을 열 때 이를 제공하십시오**                      |
| `other`                               | 문자열 | 추가 과금 및 요청 세부 정보, **다시 파싱해야 하는 JSON 문자열**             |

<Info>
  `other` 필드는 중첩 객체가 아니라 JSON **문자열**을 담고 있으므로, 두 번째 파싱이 필요합니다
  (`json.loads()` in Python, `JSON.parse()` in JavaScript). 여기에는 `billing_type`,
  `request_path`(실제로 호출된 엔드포인트), `group_ratio`, `model_ratio`, `usage`가 포함됩니다.
</Info>

### 쿼터 변환

<Card title="변환 규칙" icon="calculator">
  500,000 쿼터 = \$1.00 USD
</Card>

**공식:** USD 금액 = `quota` ÷ 500,000

**예시:**

* `quota: 7500` → \$0.015 USD
* `quota: 22500` → \$0.045 USD
* `quota: 18` → \$0.000036 USD

이것은 [잔액 조회 API](/ko/api-capabilities/balance-query)에서 사용하는 것과 동일한 변환이므로,
두 값이 직접적으로 일치합니다.

## 오류 응답

### HTTP 401 - 인증 실패

```json theme={null}
{
  "success": false,
  "message": "You are not authorized to perform this operation. The access token is invalid."
}
```

**이유:** 시스템 token이 유효하지 않거나 만료되었거나, `sk-`로 시작하는 API key를
실수로 시스템 token으로 사용한 경우입니다.

**해결 방법:** 콘솔에서 시스템 token을 다시 생성하고, `Authorization`에 `Bearer` 접두사가
없는 원시 값이 들어 있는지 확인하십시오.

## 코드 예제

### cURL 예제(단일 페이지, 빠른 확인)

```bash theme={null}
export APIYI_SYS_TOKEN='YOUR_SYSTEM_TOKEN'

curl --compressed -s 'https://api.apiyi.com/api/log/self?p=0&page_size=10&type=2' \
  -H "Authorization: $APIYI_SYS_TOKEN" \
  -H 'Accept: application/json' | jq '.data[] | {created_at, model_name, quota, request_id}'
```

<Warning>
  **`--compressed` 옵션은 필수입니다**, API가 gzip으로 압축된 콘텐츠를 반환하기 때문입니다.
  이 옵션이 없으면 출력이 깨져 보입니다.
</Warning>

<Info>
  이 명령은 최대 10개 레코드만 반환합니다. 실제 대조 작업에는 아래의 페이지네이션 버전을 사용하십시오.
</Info>

### Python 예제(페이지네이션, 바로 실행 가능)

```python theme={null}
import json
import os
import time
from collections import defaultdict

import requests

BASE = "https://api.apiyi.com"
TOKEN = os.environ["APIYI_SYS_TOKEN"]
QUOTA_PER_USD = 500_000

HEADERS = {"Authorization": TOKEN, "Accept": "application/json"}


def fetch_logs(hours=24, model_name=None):
    """Fetch consumption logs for the last N hours, paginating automatically."""
    now = int(time.time())
    params = {
        "page_size": 10,          # server-side cap is 10; larger values have no effect
        "type": 2,                # 2 = consumption, the only type used for reconciliation
        "start_timestamp": now - hours * 3600,
        "end_timestamp": now,
    }
    if model_name:
        params["model_name"] = model_name

    rows, seen = [], set()
    page = 0
    while True:
        resp = requests.get(f"{BASE}/api/log/self",
                            headers=HEADERS, params={**params, "p": page}, timeout=30)
        resp.raise_for_status()
        data = resp.json().get("data") or []
        if not data:
            break                 # an empty array means we reached the end

        fresh = 0
        for row in data:
            # some records always report id as 0, so deduplicate on request_id instead
            key = row.get("request_id")
            if key in seen:
                continue
            seen.add(key)
            rows.append(row)
            fresh += 1
        if fresh == 0:
            break                 # whole page was duplicates, defensive exit
        page += 1
    return rows


def summarize(rows):
    """Aggregate call count and spend per model."""
    stat = defaultdict(lambda: {"count": 0, "quota": 0})
    for row in rows:
        s = stat[row.get("model_name") or "(none)"]
        s["count"] += 1
        s["quota"] += row.get("quota") or 0

    total = sum(s["quota"] for s in stat.values())
    print(f"{'MODEL':32s} {'CALLS':>6s} {'SPEND(USD)':>12s}")
    for model, s in sorted(stat.items(), key=lambda kv: -kv[1]["quota"]):
        print(f"{model:32s} {s['count']:6d} {s['quota'] / QUOTA_PER_USD:12.4f}")
    print(f"\n{len(rows)} calls, {total:,} quota = ${total / QUOTA_PER_USD:.4f} USD")


if __name__ == "__main__":
    logs = fetch_logs(hours=24)
    summarize(logs)

    # other is a JSON string and needs a second parse
    if logs:
        extra = json.loads(logs[0].get("other") or "{}")
        print("\nEndpoint of the most recent call:", extra.get("request_path"))
```

**샘플 출력:**

```
MODEL                             CALLS   SPEND(USD)
gpt-5.6                             128       2.3850
gemini-3-pro-image                   30       1.3500
deepseek-chat                       412       0.0148

570 calls, 1,867,400 quota = $3.7348 USD
```

### Node.js 예제(페이지네이션)

```javascript theme={null}
const BASE = "https://api.apiyi.com";
const TOKEN = process.env.APIYI_SYS_TOKEN;
const QUOTA_PER_USD = 500_000;

async function fetchLogs({ hours = 24, modelName = null } = {}) {
  const now = Math.floor(Date.now() / 1000);
  const base = {
    page_size: "10",           // server-side cap is 10
    type: "2",                 // 2 = consumption
    start_timestamp: String(now - hours * 3600),
    end_timestamp: String(now),
  };
  if (modelName) base.model_name = modelName;

  const rows = [];
  const seen = new Set();
  for (let page = 0; ; page += 1) {
    const qs = new URLSearchParams({ ...base, p: String(page) });
    const resp = await fetch(`${BASE}/api/log/self?${qs}`, {
      headers: { Authorization: TOKEN, Accept: "application/json" },
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    const { data } = await resp.json();
    if (!data || data.length === 0) break;

    let fresh = 0;
    for (const row of data) {
      // deduplicate on request_id, since some records always report id as 0
      if (seen.has(row.request_id)) continue;
      seen.add(row.request_id);
      rows.push(row);
      fresh += 1;
    }
    if (fresh === 0) break;
  }
  return rows;
}

const logs = await fetchLogs({ hours: 24 });
const total = logs.reduce((sum, r) => sum + (r.quota || 0), 0);
console.log(`${logs.length} calls, $${(total / QUOTA_PER_USD).toFixed(4)} USD`);
```

<Info>
  Python requests 라이브러리와 Node.js fetch API는 모두 gzip을 자동으로 압축 해제하므로,
  별도의 설정이 필요하지 않습니다. curl만 명시적인 `--compressed` 플래그가 필요합니다.
</Info>

## 공통 시나리오

### 특정 기간의 지출 계산

위의 Python 예제를 사용하십시오. 중요한 두 가지는 `type=2`을 전달하는 것과 합산된 `quota`을 500,000으로 나누는 것입니다. 이를 단일 모델로 범위를 제한하려면 `model_name` 매개변수를 추가하십시오.

### 실패한 호출 찾기

```python theme={null}
failed = [r for r in fetch_logs(hours=24) if r.get("error_code")]
for r in failed:
    print(r["created_at"], r["model_name"], r["error_code"], r["request_id"])
```

<Info>
  게이트웨이에서 거부된 요청(잘못된 매개변수 등)은 `quota`가 0이며
  **과금되지 않습니다**. 로그의 `error_code`를 사용하면 “호출이 실패함”과
  “호출은 성공했지만 결과가 마음에 들지 않음”을 구분할 수 있습니다.
</Info>

### 지원을 위한 요청 ID 제공

로그에서 문제가 있는 호출을 찾아 지원팀에 `request_id`를 제공하십시오. 그러면 정확한 요청을 끝에서 끝까지 식별할 수 있으므로, “어떤 모델에 대한 호출이 특정 시점에 실패했다”라고 설명하는 것보다 훨씬 효율적입니다.

## 자주 묻는 질문

<AccordionGroup>
  <Accordion title="왜 10개 레코드만 반환됩니까?">
    서버 측에서 `page_size`의 상한은 10입니다. 더 큰 값을 넣어도 오류는 나지 않지만 적용되지도 않습니다. 더 많이 가져오려면 응답이 빈 배열을 반환할 때까지 페이지네이션(`p=0`, `p=1`, `p=2` 등)을 해야 합니다. 위의 Python 및 Node.js 예제에는 이미 이 내용이 포함되어 있습니다.
  </Accordion>

  <Accordion title="응답의 일부 필드가 비어 있습니다. 문제가 있습니까?">
    아니요. 일부 필드는 플랫폼 내부 정보를 담고 있으며 일반 계정의 관점에서는 비어 있거나 0입니다. 이는 예상된 동작이며, 정산이나 문제 해결에 필요한 필드에는 영향을 주지 않습니다 — `quota`, `model_name`, `error_code`, `request_id`은 모두 완전히 채워져 있습니다.
  </Accordion>

  <Accordion title="token_group이 콘솔에 표시되는 그룹 이름과 다른 이유는 무엇입니까?">
    API는 그룹 **식별자**를 반환하는 반면, 콘솔은 그룹의 **레이블**을 표시합니다. 이 둘은 다를 수 있습니다 — 예를 들어 API는 `default`을 반환하지만 콘솔에는 Default가 표시됩니다.

    전체 매핑은 공개 엔드포인트 `https://api.apiyi.com/api/pricing`의 `usable_group` 필드에서 확인할 수 있으며, 이 필드는 식별자를 레이블에 매핑합니다. 보고서가 콘솔과 일치하도록 하려면 해당 매핑을 직접 적용하십시오.
  </Accordion>

  <Accordion title="token 수와 쿼터가 서로 맞지 않는 것 같습니다. 무엇이 기준입니까?">
    `quota`을 사용하십시오. 이는 호출에서 **실제로 차감된** 금액이며, 정산에 적합한 유일한 필드입니다. 이미지 생성 및 동영상 생성처럼 호출당 과금되는 모델의 경우, 응답의 token 수는 과금에 참여하지 않는 자리표시자 값일 수 있습니다 — 해당 모델은 `by_count`을 `other.billing_type`에서 보고합니다.
  </Accordion>

  <Accordion title="얼마나 과거까지 조회할 수 있습니까?">
    `start_timestamp`와 `end_timestamp`로 원하는 범위를 지정하십시오. 이력 데이터의 정확한 보존 기간은 지원팀에 문의해 주십시오. 장기 조회를 위해 API에 의존하기보다 정산 데이터를 주기적으로 내보내는 것을 권장합니다.
  </Accordion>

  <Accordion title="curl이 깨진 텍스트를 반환하거나 jq가 오류를 발생시킵니다">
    **원인:** API가 gzip으로 압축된 콘텐츠(`Content-Encoding: gzip`)를 반환하며 curl이 이를 압축 해제하지 않기 때문입니다.

    **해결 방법:** `--compressed` 플래그를 추가하십시오:

    ```bash theme={null}
    curl --compressed 'https://api.apiyi.com/api/log/self?p=0' \
      -H "Authorization: $APIYI_SYS_TOKEN" | jq
    ```

    Python requests 라이브러리와 Node.js fetch API는 자동으로 압축을 해제합니다.
  </Accordion>

  <Accordion title="로그를 조회하면 쿼터가 소모됩니까?">
    아니요. 로그 조회 엔드포인트는 어떤 쿼터도 사용하지 않습니다.
  </Accordion>
</AccordionGroup>

## 중요 사항

<Warning>
  **시스템 토큰은 API 키가 아니며, 둘은 서로 호환되지 않습니다**

  * **API 키**(접두사가 `sk-`으로 시작함)는 `/v1/*` 추론 엔드포인트용입니다. 이를 `/api/log/self`에 사용하면 401을 반환합니다.
  * **시스템 토큰**(접두사가 없는 일반 문자열)은 `/api/*` 관리 엔드포인트용입니다. 이를 `/v1/chat/completions`에 사용하면 잘못된 토큰 오류가 반환됩니다.

  시스템 토큰의 범위는 계정 전체에 적용되므로, **계정 비밀번호처럼 취급하십시오**:
  코드가 아니라 시크릿 관리자에 저장하고, 저장소에 절대 커밋하지 말며, 주기적으로 교체하십시오.
</Warning>

<Warning>
  **로그 응답에는 사용자의 API 키가 평문으로 포함됩니다**

  각 로그 레코드에는 해당 호출을 만든 토큰에 대한 정보가 포함됩니다. **원시 로그 응답을 공개된 곳에 붙여넣거나, 스크린샷을 공유하거나, 제3자에게 넘기지 마십시오** — 내보내기 전에 민감한 필드를 제거하십시오.

  특히 이 평문에는 `sk-` 접두사가 포함되지 않으므로, **일반적인 시크릿 스캐너로는 탐지되지 않을 수 있습니다**. 자동 검사를 그대로 믿고 놓치지 않도록 하십시오.
</Warning>

<Info>
  **요청 제한**

  * 요청 제한을 피하려면 쿼리 사이를 최소 1초 이상 두십시오
  * 적절한 요청 타임아웃을 설정하십시오(30초 권장)
  * 넓은 시간 범위의 경우 페이지네이션과 재시도 처리를 구현하십시오
</Info>

<Card title="관련 문서" icon="link">
  * [잔액 조회 API](/ko/api-capabilities/balance-query) — 남은 계정 크레딧을 확인합니다
  * [토큰 관리 API](/ko/api-capabilities/token-management) — API 키를 프로그래밍 방식으로 생성하고 관리합니다
  * [내 호출 기록을 보는 방법](/ko/faq/call-logs) — 콘솔에서 수동으로 확인합니다
  * [로그와 과금 이해](/ko/faq/log-billing-explained) — 과금 필드를 읽는 방법을 설명합니다
</Card>
