# 사용량 집계 (/docs/api-reference/usage)

# 사용량 집계

계정의 사용량을 **집계된 형태로** 돌려줍니다. 통화 목록(`GET /v1/accounts/{accountId}/calls`)을 받아 직접 합산하는 것과는 결과가 다릅니다.

## 통화 목록을 직접 합산하면 안 되는 이유

통화 목록을 주기적으로 받아 자체 DB에 누적하는 방식은 **시간이 지날수록 청구서와 어긋납니다.** 아래 세 가지가 반영되지 않기 때문입니다.

1. **사후 정정** — 통화 시간은 종료 후에도 정정될 수 있습니다(지연 종료 처리, 운영 보정). 이미 받아간 건이 정정되면 자체 DB에는 반영할 방법이 없습니다. 이 API는 조회 시점에 다시 집계하므로 정정이 항상 반영됩니다.
2. **청구 주기 경계** — 청구 주기의 시작은 매월 1일이 아니라 **구독을 시작한 순간의 시각**입니다. 달력 월로 합산하면 주기 앞뒤가 섞입니다.
3. **통화 목록에 없는 미터** — 문자·전사·요약은 통화 기록에 들어 있지 않습니다.

## 미터

모든 통화 미터는 **초 단위**로 반환됩니다.

| 미터 | 단위 | 설명 |
|------|------|------|
| `outbound_seconds` | second | 발신 통화 |
| `inbound_seconds` | second | 수신 통화 |
| `transfer_seconds` | second | 통화 전환으로 발생한 외부 구간. 청구는 발신에 **합산**되므로 `billableMinutes` 는 항상 `null` 입니다. |
| `sms` / `lms` / `mms` | count | 문자 발송 건수(실패 제외) |
| `ata` | count | 카카오 알림톡 발송 건수(실패 제외) |
| `bms` | count | 카카오 브랜드 메시지 발송 건수(실패 제외) |
| `transcription_seconds` | second | 받아쓰기 |
| `summary_seconds` | second | 요약 |
| `amd_invocations` | count | 사람/기계 판별 수행 횟수 |

**왜 분이 아니라 초인가** — 청구는 계정 합계에 올림을 **한 번만** 적용합니다(`ceil((발신+전환)/60)`). 축별로 나눈 값을 각각 올림해 더하면 합계가 청구서보다 커집니다. 그래서 쪼갠 값은 초로 주고, 청구와 같은 규칙으로 올림한 분은 `totals[].billableMinutes` 에만 담습니다.

`meter=outbound_seconds` 만 요청하셔도 **전환 초는 `billableMinutes` 계산에 포함됩니다**(요청하지 않으셨다면 `records`·`totals` 에는 나오지 않습니다). 필터를 어떻게 거시든 발신의 청구 분은 청구서와 같은 값입니다.

## 집계 축

`groupBy` 로 축을 지정합니다. 여러 축은 파라미터를 반복해 조합합니다(`?groupBy=day&groupBy=linkId`). 콤마 구분(`?groupBy=day,linkId`)은 지원하지 않습니다 — 오타를 조용히 넘기지 않기 위해 값 검증을 유지한 결과입니다.

| 축 | 의미 |
|----|------|
| `meter` | 미터별 합계(기본값) |
| `day` | 일자별(UTC) |
| `linkId` | **관리번호 발급 링크별** — 최종이용자 단위 |
| `number` | 전화번호별 |

### 정산에는 `linkId` 를 쓰십시오

`linkId` 는 [`assignment.completed` Webhook](/docs/webhooks/assignment) 의 `LinkId` 와 같은 값입니다. 발급 링크를 만든 쪽이 이미 쥐고 있는 값이므로, 자체 시스템의 고객 레코드와 바로 연결할 수 있습니다. 관리번호로 발급되지 않은 번호(직접 발급분)는 `linkId` 가 `null` 로 묶입니다.

`linkId` 는 발급 링크 주소에 포함되는 값과 동일합니다. **정산서·CSV 등 외부로 나가는 문서에 그대로 싣지 마십시오.** 자체 시스템에서 고객 레코드와 연결하는 용도로만 사용하시고, 외부 공유 시에는 자사 고객 ID 로 치환하시기 바랍니다.

**전화번호를 정산 키로 사용하지 마십시오.** 번호는 반납 후 다른 이용자에게 재배정됩니다. 이 API 는 배정 구간을 기준으로 잘라 집계하므로 응답에는 이전 이용자의 사용량이 섞이지 않지만, 수신한 값을 번호를 키로 누적하면 그 구분이 사라집니다.

## 기간

`from`/`to` 를 생략하면 **현재 청구 주기** 전체를 집계합니다. 지정할 때는 둘 다 주어야 하며, `to` 는 포함되지 않습니다. 최대 구간은 92일입니다.

모든 경계와 `day` 값은 **UTC** 기준입니다. 응답의 `period` 를 그대로 정산 근거로 사용하시면 청구서와 일치합니다.

| 필드 | 의미 |
|------|------|
| `confirmedThrough` | 이 날짜까지는 정정이 반영된 확정값입니다. |
| `includesToday` | 진행 중인 오늘 구간 포함 여부. `includeToday=true` 일 때만 참입니다. |

## 엔드포인트

| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/v1/accounts/{accountId}/usage` | 사용량 집계 조회 |

### 쿼리 파라미터

| 이름 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| `from` / `to` | date-time | 현재 청구 주기 | 둘 다 지정. `to` 미포함. 최대 92일 |
| `groupBy` | string[] | `meter` | `meter` `day` `linkId` `number`. 반복 지정 |
| `meter` | string[] | 전체 | 특정 미터만 조회. 반복 지정 |
| `includeToday` | boolean | `false` | 오늘(UTC) 구간 포함 여부 |
| `pageSize` / `page` | integer | 200 / 1 | `page_size`·`limit` 별칭 지원 |

## 기본 조회

```bash
curl "https://api.claw-ops.com/v1/accounts/AC.../usage" \
  -H "Authorization: Bearer sk_..."
```

응답 (200):

```json
{
  "period": {
    "from": "2026-08-03T03:56:49.081Z",
    "to": "2026-09-03T03:56:49.081Z",
    "timezone": "UTC",
    "confirmedThrough": "2026-08-20",
    "includesToday": false
  },
  "records": [
    { "meter": "outbound_seconds", "unit": "second", "quantity": 24680 },
    { "meter": "transfer_seconds", "unit": "second", "quantity": 1820 },
    { "meter": "inbound_seconds", "unit": "second", "quantity": 512400 },
    { "meter": "sms", "unit": "count", "quantity": 88 }
  ],
  "meta": { "page": 1, "pageSize": 200, "total": 4 },
  "totals": [
    { "meter": "outbound_seconds", "unit": "second", "quantity": 24680,
      "billableMinutes": 442, "unitPriceHint": null },
    { "meter": "inbound_seconds", "unit": "second", "quantity": 512400,
      "billableMinutes": 8540, "unitPriceHint": null },
    { "meter": "sms", "unit": "count", "quantity": 88,
      "billableMinutes": null, "unitPriceHint": null }
  ],
  "amount": null
}
```

## 최종이용자별 조회

```bash
curl "https://api.claw-ops.com/v1/accounts/AC.../usage?groupBy=linkId&meter=outbound_seconds&meter=sms" \
  -H "Authorization: Bearer sk_..."
```

```json
{
  "period": { "from": "2026-08-03T03:56:49.081Z", "to": "2026-09-03T03:56:49.081Z",
              "timezone": "UTC", "confirmedThrough": "2026-08-20", "includesToday": false },
  "records": [
    { "meter": "outbound_seconds", "unit": "second", "linkId": "AL7f2c9a1b", "quantity": 12400 },
    { "meter": "sms", "unit": "count", "linkId": "AL7f2c9a1b", "quantity": 40 },
    { "meter": "outbound_seconds", "unit": "second", "linkId": "AL0b31de77", "quantity": 9200 },
    { "meter": "outbound_seconds", "unit": "second", "linkId": null, "quantity": 3080 }
  ],
  "totals": [
    { "meter": "outbound_seconds", "unit": "second", "quantity": 24680, "billableMinutes": 442 },
    { "meter": "sms", "unit": "count", "quantity": 88 }
  ],
  "amount": null
}
```

## 일자 × 번호 조회

```bash
curl "https://api.claw-ops.com/v1/accounts/AC.../usage?groupBy=day&groupBy=number&from=2026-08-01T00:00:00Z&to=2026-08-08T00:00:00Z" \
  -H "Authorization: Bearer sk_..."
```

```json
{
  "records": [
    { "meter": "inbound_seconds", "unit": "second",
      "day": "2026-08-01", "phoneNumber": "07052753941", "quantity": 8210 }
  ]
}
```

## 금액

`records` 에는 **수량만** 담깁니다. 통화 단가는 사용량 구간에 따라 달라지는 누진 구조라, 축별로 금액을 나눌 수 없습니다 — 금액은 합계에만 정의됩니다.

**`amount` 와 `totals[].unitPriceHint` 는 현재 항상 `null` 입니다.** 금액 계산은 플랜 포함량·누진 구간·애드온을 함께 봐야 하므로 후속 작업으로 남아 있습니다. 지금은 **수량**을 기준으로 연동하십시오 — 이후 값이 채워져도 필드 형식은 그대로입니다.

- `totals[].unitPriceHint` — (예정) 조회 시점에 그 미터가 놓인 구간의 단가(원, 부가세 포함)입니다. 사용량이 늘어 다음 구간으로 넘어가면 달라지며, 플랜 포함량 안에 있으면 `null` 입니다. 이 값을 수량에 곱한 것은 청구액이 아닙니다.
- `amount` — (예정) 계정 전체의 종량 예상액입니다. 축을 지정한 조회(`groupBy` 에 `linkId`·`number`·`day` 포함)에서는 채워진 뒤에도 `null` 입니다. 확정 청구액이 아니며, 플랜 기본료·애드온·일할 계산은 포함되지 않습니다.

최종이용자에게 재청구하실 때는 이 API 의 **수량**에 자사 단가를 적용하시는 것이 정확합니다. 우리 쪽 포함량과 구간 단가는 [요금](/docs/api-reference/pricing) 에 있습니다.

## 월 정산 예시

1. 청구 주기가 끝난 뒤 `GET /usage?groupBy=linkId` 를 호출합니다(기간 생략 시 현재 주기이므로, 지난 주기는 `from`/`to` 로 지정).
2. 응답의 `period` 를 정산서에 그대로 기재합니다 — 고객 문의 시 대조 근거가 됩니다.
3. `records` 의 초 단위 수량에 자사 정책으로 올림·단가를 적용합니다.
4. `totals[].billableMinutes` 합계가 한국AI통신 청구서의 사용량과 일치하는지 확인합니다.

## 페이징

`records` 는 `page`/`pageSize` 로 나뉘며, `meta.total` 이 페이징 전 전체 행 수입니다. 마지막 페이지인지는 `page * pageSize >= meta.total` 로 판단하시면 됩니다 — 빈 페이지가 나올 때까지 요청하실 필요가 없습니다.

`totals` 와 `meta.total` 은 다른 값입니다. `totals` 는 미터별 사용량 합계, `meta.total` 은 행 개수입니다. **`totals` 는 페이지와 무관하게 항상 전체 구간 기준**이라 첫 페이지만 받아도 정산 대조에 쓸 수 있습니다.

## MCP 도구

연결한 AI 에이전트에서도 같은 집계를 부를 수 있습니다 — 도구 이름은 `get_usage`, 필요한 권한은 `read:usage` 입니다. 인자는 이 API 의 쿼리 파라미터와 같습니다(`from`·`to`·`groupBy`·`meter`·`includeToday`·`page`·`pageSize`).

**기존에 연결된 앱은 재연결해야 이 도구가 보입니다.** 권한은 연결할 때 확정되므로, `read:usage` 가 추가되기 전에 연결한 앱은 이 도구를 목록에서 받지 못합니다(에러가 아니라 도구가 없는 상태로 동작합니다).

`records` 는 모델 컨텍스트를 고려해 **기본 50행**만 실립니다(REST 기본값은 200). 전체 행 수는 `meta.total` 로 확인하시고, 더 필요하면 `pageSize`·`page` 를 지정하십시오. `totals` 는 여기서도 페이지와 무관한 전체 구간 합계입니다.

## 제한

- 조회 구간 최대 92일
- `includeToday=true` 는 진행 중인 구간을 원본에서 직접 집계하므로 응답이 느립니다. 정산에는 확정 구간만 사용하시는 것을 권장합니다.
