# 사용 내역 내보내기

> LMU AI 사용량 내보내기 가이드: 로그인하여 JWT를 발급받은 뒤 페이지네이션, 날짜 범위, 모델 필터링과 함께 /api/v1/usage를 호출하며, 세 가지 언어의 예제를 제공합니다.

URL: https://docs.lmuai.com/ko/docs/api/usage-export



계정과 비밀번호로 로그인하여 JWT를 발급받은 뒤, 그 JWT로 `/api/v1/usage`를 호출하면 **콘솔 `/usage` 페이지와 동일한 전체 토큰 사용 내역 테이블**을 가져올 수 있습니다.

내보내기, 대사(reconciliation), 엔터프라이즈 BI 적재를 자동화하는 장시간 실행 스크립트에 이상적이며, 매번 수동으로 CSV를 다운로드할 필요가 없습니다.

<Callout type="info">
  **왜 `sk-...` API 키로 직접 가져올 수 없나요?** 이 키는 게이트웨이 비즈니스 호출(/v1/messages 등)에만 사용되며 계정 수준의 상세 조회는 노출하지 않습니다. 이는 의도적인 보안 설계입니다. 키가 유출되더라도 전체 청구 내역이 통째로 노출되는 것을 원치 않기 때문입니다. 계정 수준 데이터는 반드시 계정 인증(JWT)을 사용해야 합니다.
</Callout>

***

## 1. API 개요 [#1-api-개요]

| 항목           | 값                                                          |
| ------------ | ---------------------------------------------------------- |
| Base URL     | `https://api.lmuai.com`                                    |
| 인증           | `Authorization: Bearer <access_token>` (계정 로그인으로 발급받은 JWT) |
| 액세스 토큰 유효 기간 | **86400초 (24시간)**, refresh\_token으로 갱신 가능                  |
| 응답 형식        | JSON; `code: 0`은 성공, 실패 시 `code != 0`과 함께 `message` 필드     |
| 권장 용도        | 자동화된 청구 내보내기, 엔터프라이즈 BI 적재, 대사 스크립트                        |

***

## 2. 1단계 — 로그인하여 JWT 발급받기 [#2-1단계--로그인하여-jwt-발급받기]

### `POST /api/v1/auth/login` [#post-apiv1authlogin]

```http
POST /api/v1/auth/login HTTP/1.1
Content-Type: application/json

{
  "email": "your@email.com",
  "password": "your-password"
}
```

**응답** (발췌):

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "access_token": "eyJhbGc...",
    "refresh_token": "rt_756a220ffa...",
    "expires_in": 86400,
    "token_type": "Bearer",
    "user": { "id": 3, "email": "...", "balance": 1575.13 }
  }
}
```

이후의 모든 요청에는 `Authorization: Bearer <access_token>`을 포함하기만 하면 됩니다.

***

## 3. 2단계 — 사용 내역 가져오기 [#3-2단계--사용-내역-가져오기]

### `GET /api/v1/usage` [#get-apiv1usage]

```http
GET /api/v1/usage?page=1&page_size=200&start_date=2026-06-01&end_date=2026-06-23&timezone=Asia/Shanghai
Authorization: Bearer <access_token>
```

### 쿼리 파라미터 [#쿼리-파라미터]

| 파라미터           | 타입     | 필수  | 기본값          | 설명                                                                           |
| -------------- | ------ | --- | ------------ | ---------------------------------------------------------------------------- |
| `page`         | int    | 아니오 | 1            | 페이지 번호, 1부터 시작                                                               |
| `page_size`    | int    | 아니오 | 20           | 페이지당 행 수, 200 권장 (과도한 값은 잘림)                                                 |
| `start_date`   | string | 아니오 | —            | 시작 날짜 `YYYY-MM-DD`, `timezone`으로 파싱                                          |
| `end_date`     | string | 아니오 | —            | 종료 날짜 (해당 날짜 포함)                                                             |
| `timezone`     | string | 아니오 | UTC          | 날짜 파라미터를 파싱할 시간대, 예: `Asia/Shanghai`; **강력히 권장**, 그렇지 않으면 경계가 로컬 시간과 일치하지 않음 |
| `api_key_id`   | int    | 아니오 | —            | 특정 키 하나의 내역만 조회 (현재 사용자에게 속해야 함)                                             |
| `model`        | string | 아니오 | —            | 모델 ID로 필터링 (예: `claude-sonnet-5`)                                            |
| `stream`       | bool   | 아니오 | —            | `true`는 스트리밍만, `false`는 비스트리밍만                                               |
| `billing_type` | int    | 아니오 | —            | 청구 유형 열거값 (아래 참조)                                                            |
| `sort_by`      | string | 아니오 | `created_at` | 정렬 필드                                                                        |
| `sort_order`   | string | 아니오 | `desc`       | `asc` 또는 `desc`                                                              |

### 응답 구조 [#응답-구조]

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "items": [ { /* UsageLog */ }, ... ],
    "total": 30423,
    "page": 1,
    "page_size": 200,
    "pages": 153
  }
}
```

<Callout type="warn">
  **페이지네이션 필드는 외부 엔벌로프가 아니라 `data`의 최상위 레벨에 있습니다**. 상세 배열은 `data.items`에 있으며 — `data` 자체는 배열이 아닙니다.
</Callout>

### 단일 레코드의 필드 매핑 [#단일-레코드의-필드-매핑]

이들은 콘솔 `/usage` 페이지의 각 열과 정확히 대응됩니다:

| 콘솔 열           | JSON 필드                                                                    | 타입             | 설명                                                     |
| -------------- | -------------------------------------------------------------------------- | -------------- | ------------------------------------------------------ |
| **모델**         | `model`                                                                    | string         | 실제로 청구된 모델 ID                                          |
| **추론 강도**      | `reasoning_effort`                                                         | string \| null | `low` / `medium` / `high` / `xhigh` / `max`            |
| **인바운드 엔드포인트** | `inbound_endpoint`                                                         | string \| null | 클라이언트가 호출한 경로, 예: `/v1/messages`                       |
| **업스트림 엔드포인트** | `upstream_endpoint`                                                        | string \| null | 정규화된 업스트림 경로                                           |
| **요청 유형**      | `request_type`                                                             | string         | `chat` / `stream` / `responses` / `messages` 등         |
| **스트리밍**       | `stream`                                                                   | bool           | 스트리밍 여부                                                |
| **청구 모드**      | `billing_mode`                                                             | string         | `token` / `per_request` / `image` / `subscription`     |
| **청구 유형**      | `billing_type`                                                             | int            | 청구 유형 열거값                                              |
| **배수**         | `rate_multiplier`                                                          | float          | 이 요청에 적용된 채널 배수                                        |
| **입력 토큰**      | `input_tokens`                                                             | int            |                                                        |
| **출력 토큰**      | `output_tokens`                                                            | int            |                                                        |
| **캐시 생성**      | `cache_creation_tokens`                                                    | int            | 모든 캐시 쓰기                                               |
| **캐시 생성 5m**   | `cache_creation_5m_tokens`                                                 | int            | Anthropic 임시 5분 계층                                     |
| **캐시 생성 1h**   | `cache_creation_1h_tokens`                                                 | int            | Anthropic 1시간 계층                                       |
| **캐시 읽기**      | `cache_read_tokens`                                                        | int            |                                                        |
| **입력 비용**      | `input_cost`                                                               | float          | USD                                                    |
| **출력 비용**      | `output_cost`                                                              | float          | USD                                                    |
| **캐시 생성 비용**   | `cache_creation_cost`                                                      | float          | USD                                                    |
| **캐시 읽기 비용**   | `cache_read_cost`                                                          | float          | USD                                                    |
| **정가 총액**      | `total_cost`                                                               | float          | USD, 배수 적용 전                                           |
| **실제 청구액**     | `actual_cost`                                                              | float          | **= total\_cost × rate\_multiplier**, 잔액에서 실제로 차감되는 금액 |
| **첫 토큰 지연**    | `first_token_ms`                                                           | int \| null    | TTFT, 스트리밍에서 유효                                        |
| **총 소요 시간**    | `duration_ms`                                                              | int \| null    |                                                        |
| **시간**         | `created_at`                                                               | ISO8601        | 서버 측 기록 시간                                             |
| **요청 ID**      | `request_id`                                                               | string         | 문제 해결 및 추적용                                            |
| **이미지 생성**     | `image_count` / `image_size` / `image_output_tokens` / `image_output_cost` | —              | 멀티모달 요청에만 채워짐                                          |
| **API Key**    | `api_key_id` + 중첩된 `api_key` 객체                                            | int + obj      |                                                        |
| **그룹**         | `group_id` + 중첩된 `group` 객체                                                | int + obj      |                                                        |

<Callout type="warn">
  각 레코드에는 중첩된 `user` / `api_key` / `group&#x60; 객체가 포함됩니다 (행당 약 +1KB). **`api_key` 객체는 전체 키 텍스트를 포함하므로, 스크립트 로그 / 스크린샷 / CSV에서 반드시 마스킹하세요**. 대량 내보내기 시 중첩 객체가 필요 없다면 `jq`나 클라이언트 측 필드 매핑을 사용해 필요한 필드만 유지하세요.
</Callout>

### 청구 유형 열거값 (billing\_type) [#청구-유형-열거값-billing_type]

| 값   | 의미                |
| --- | ----------------- |
| `0` | 토큰 기준 청구 (대다수 요청) |
| `1` | 구독 할당량에서 차감       |

`billing_mode` 필드는 문자열 버전으로, 구체적인 가격 모델(`token` / `per_request` / `image` / `subscription` 등)을 설명합니다. 더 읽기 쉬우므로 분류에는 이 필드를 사용하는 것이 권장됩니다.

***

## 4. 토큰 갱신 [#4-토큰-갱신]

액세스 토큰은 24시간 후 만료됩니다. 갱신하는 방법은 두 가지입니다:

### refresh\_token으로 갱신 (스크립트에 권장) [#refresh_token으로-갱신-스크립트에-권장]

```http
POST /api/v1/auth/refresh
Content-Type: application/json

{
  "refresh_token": "rt_756a220ffa..."
}
```

새 `access_token`을 반환합니다 (보통 `refresh_token`도 함께 순환됩니다). refresh 토큰은 30일간 유효합니다.

### 다시 로그인 [#다시-로그인]

투박하지만 단순한 방법으로, 일회성 스크립트에 적합합니다. 매 실행 전에 `/auth/login`을 다시 호출합니다.

***

## 5. 코드 예제 [#5-코드-예제]

### Bash + curl + jq [#bash--curl--jq]

```bash
#!/usr/bin/env bash
set -e

EMAIL="${EMAIL:?set EMAIL env}"
PASSWORD="${PASSWORD:?set PASSWORD env}"
BASE="https://api.lmuai.com"

JWT=$(curl -s -X POST "$BASE/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
  | jq -r '.data.access_token')

[ -n "$JWT" ] && [ "$JWT" != "null" ] || { echo "login failed" >&2; exit 1; }

START="2026-06-01"; END="2026-06-30"; TZ="Asia/Shanghai"; PAGE_SIZE=200

# Fetch page 1 to get total
META=$(curl -s "$BASE/api/v1/usage?page=1&page_size=$PAGE_SIZE&start_date=$START&end_date=$END&timezone=$TZ" \
  -H "Authorization: Bearer $JWT")
PAGES=$(echo "$META" | jq -r '.data.pages')

# CSV header
echo "created_at,model,request_type,input_tokens,output_tokens,cache_read_tokens,total_cost,actual_cost,duration_ms" > usage.csv

# Write page 1
echo "$META" | jq -r '.data.items[] | [.created_at, .model, .request_type, .input_tokens, .output_tokens, .cache_read_tokens, .total_cost, .actual_cost, .duration_ms] | @csv' >> usage.csv

# Fetch remaining pages
for ((p=2; p<=PAGES; p++)); do
  curl -s "$BASE/api/v1/usage?page=$p&page_size=$PAGE_SIZE&start_date=$START&end_date=$END&timezone=$TZ" \
    -H "Authorization: Bearer $JWT" \
    | jq -r '.data.items[] | [.created_at, .model, .request_type, .input_tokens, .output_tokens, .cache_read_tokens, .total_cost, .actual_cost, .duration_ms] | @csv' >> usage.csv
done

echo "saved $(wc -l < usage.csv) lines to usage.csv"
```

### Python [#python]

```python
import os, csv, requests

BASE = "https://api.lmuai.com"
EMAIL = os.environ["EMAIL"]
PASSWORD = os.environ["PASSWORD"]

def login() -> str:
    r = requests.post(f"{BASE}/api/v1/auth/login",
                      json={"email": EMAIL, "password": PASSWORD},
                      timeout=15)
    r.raise_for_status()
    data = r.json()
    if data["code"] != 0:
        raise RuntimeError(data.get("message", "login failed"))
    return data["data"]["access_token"]

def fetch_usage(jwt: str, **params):
    """Yield each row from paginated /usage endpoint."""
    page = 1
    while True:
        r = requests.get(f"{BASE}/api/v1/usage",
                         headers={"Authorization": f"Bearer {jwt}"},
                         params={**params, "page": page, "page_size": 200},
                         timeout=30)
        r.raise_for_status()
        d = r.json()["data"]
        for row in d.get("items", []):
            yield row
        if page >= d.get("pages", 1):
            return
        page += 1

if __name__ == "__main__":
    jwt = login()
    cols = ["created_at","model","request_type","input_tokens","output_tokens",
            "cache_read_tokens","total_cost","actual_cost","duration_ms"]
    with open("usage.csv", "w", newline="") as f:
        w = csv.writer(f); w.writerow(cols)
        for row in fetch_usage(jwt, start_date="2026-06-01", end_date="2026-06-30",
                               timezone="Asia/Shanghai"):
            w.writerow([row.get(c) for c in cols])
    print("done")
```

### Node.js [#nodejs]

```js
import fs from 'node:fs'

const BASE = 'https://api.lmuai.com'
const { EMAIL, PASSWORD } = process.env

async function login() {
  const r = await fetch(`${BASE}/api/v1/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
  })
  const j = await r.json()
  if (j.code !== 0) throw new Error(j.message || 'login failed')
  return j.data.access_token
}

async function* fetchUsage(jwt, params) {
  for (let page = 1; ; page++) {
    const q = new URLSearchParams({ ...params, page, page_size: '200' })
    const r = await fetch(`${BASE}/api/v1/usage?${q}`, {
      headers: { Authorization: `Bearer ${jwt}` },
    })
    const j = await r.json()
    for (const row of j.data.items) yield row
    if (page >= j.data.pages) return
  }
}

const jwt = await login()
const cols = ['created_at','model','request_type','input_tokens','output_tokens',
              'cache_read_tokens','total_cost','actual_cost','duration_ms']
const out = fs.createWriteStream('usage.csv')
out.write(cols.join(',') + '\n')
for await (const row of fetchUsage(jwt, {
  start_date: '2026-06-01', end_date: '2026-06-30', timezone: 'Asia/Shanghai',
})) {
  out.write(cols.map(c => JSON.stringify(row[c] ?? '')).join(',') + '\n')
}
console.log('done')
```

***

## 6. 실전 참고 사항 [#6-실전-참고-사항]

1. **대량 페이지 넘김 사이에 지연을 추가하세요** — N개의 페이지를 연속으로 넘길 때는 속도 제한을 유발하지 않도록 `sleep(0.1~0.3s)`가 권장됩니다.
2. **한 번에 너무 긴 시간 범위를 가져오지 마세요** — 여러 달에 걸쳐 수백만 행에 이르면 느립니다. 월별로 나누는 것이 더 안정적입니다.
3. **`page_size`는 200 권장** — 서버는 과도한 값을 잘라냅니다.
4. **대역폭 절약** — 중첩된 `user` / `api_key` / `group` 객체는 크므로, 상세 행만 필요하다면 클라이언트에서 `jq`로 필터링하세요.
5. **자주 발생하는 오류**:
   * `401` JWT 만료 → `refresh_token`으로 갱신하거나 다시 로그인
   * `403` 무권한 접근 (예: `api_key_id`로 다른 사람의 키를 조회) → 해당 키가 현재 사용자에게 속하는지 확인
   * `400` 잘못된 파라미터 (예: `start_date` 형식) → `YYYY-MM-DD` + `timezone`이 유효한지 확인

***

## 7. 보안 권장 사항 (필독) [#7-보안-권장-사항-필독]

<Callout type="warn">
  **계정 비밀번호 + JWT는 전체 계정 접근 권한과 동일합니다.** 내보내기를 스크립트로 작성하기 전에 반드시 확인하세요:
</Callout>

| 항목       | 권장 사항                                                                                                                         |
| -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 자격 증명 관리 | **절대로** password / access\_token / refresh\_token을 git이나 CI 로그에 커밋하지 마세요. 환경 변수, 시크릿 매니저, 또는 `.env` (`.gitignore`와 함께)를 사용하세요 |
| 계정 보안    | **TOTP 2단계 인증** 활성화를 강력히 권장합니다 (계정 설정에서 활성화). 이 API는 로그인 시 TOTP 검증을 지원합니다                                                     |
| 정기 순환    | 계정 비밀번호를 정기적으로 변경하고, refresh\_token 만료 후 즉시 순환하세요                                                                             |
| 이상 모니터링  | 계정의 "마지막 로그인 시간 / IP"를 주시하세요. 낯선 기기가 보이면 즉시 비밀번호를 변경하세요                                                                       |
| 필드 마스킹   | 응답의 중첩된 `api_key.key` (전체 sk-... 텍스트), `user.email`, `refresh_token`은 모두 민감 필드입니다 — **출력 / 로그 / CSV 전에 마스킹하세요**               |
| 최소 권한    | 분리할 수 있다면 내보내기 스크립트에 **전용 하위 계정을 사용하세요** (조직이 다중 계정 구조를 가진 경우). 이는 메인 계정의 장기 노출을 피하기 위함입니다                                    |
| 오류 재시도   | 로그인 실패 시 무한정 재시도하지 마세요 — 리스크 컨트롤을 쉽게 유발합니다. 지수 백오프가 권장됩니다                                                                     |

***

## 8. 필드 빠른 참조 [#8-필드-빠른-참조]

CSV 열 매핑에 유용합니다:

| 레이블        | 필드                            | 단위           |
| ---------- | ----------------------------- | ------------ |
| 모델         | `model`                       | —            |
| 추론 강도      | `reasoning_effort`            | —            |
| 인바운드 엔드포인트 | `inbound_endpoint`            | path         |
| 업스트림 엔드포인트 | `upstream_endpoint`           | path         |
| 요청 유형      | `request_type`                | enum         |
| 스트리밍       | `stream`                      | bool         |
| 청구 모드      | `billing_mode`                | enum         |
| 청구 유형      | `billing_type`                | int          |
| 배수         | `rate_multiplier`             | float        |
| 입력 토큰      | `input_tokens`                | int          |
| 출력 토큰      | `output_tokens`               | int          |
| 캐시 읽기 토큰   | `cache_read_tokens`           | int          |
| 캐시 쓰기 토큰   | `cache_creation_tokens`       | int          |
| 캐시 쓰기 5m   | `cache_creation_5m_tokens`    | int          |
| 캐시 쓰기 1h   | `cache_creation_1h_tokens`    | int          |
| 입력 비용      | `input_cost`                  | USD          |
| 출력 비용      | `output_cost`                 | USD          |
| 캐시 생성 비용   | `cache_creation_cost`         | USD          |
| 캐시 읽기 비용   | `cache_read_cost`             | USD          |
| 정가 총액      | `total_cost`                  | USD          |
| 실제 청구액     | `actual_cost`                 | USD          |
| 첫 토큰       | `first_token_ms`              | ms           |
| 총 소요 시간    | `duration_ms`                 | ms           |
| 시간         | `created_at`                  | ISO8601      |
| 요청 ID      | `request_id`                  | string       |
| 그룹         | `group_id` / `group.name`     | int / string |
| API Key    | `api_key_id` / `api_key.name` | int / string |
