# AICC Runtime API

게시한 에이전트에 사용자 메시지와 최근 대화 상태를 전달하고 JSON 또는 SSE로 답변을 받습니다.

## 에이전트 대화

API Key와 Agent ID는 서버에만 보관하고 브라우저에서는 고객 서버 프록시를 호출하세요.

```http
POST https://console.humelo.com/api/v1/agents/chat
```

### 인증 및 헤더

```http
Authorization: Bearer {YOUR_API_KEY}
Content-Type: application/json
Accept: application/json # SSE는 text/event-stream
```

### 요청 필드

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `agent_id` | UUID | 필수 | - | 호출할 에이전트 ID입니다. 에이전트 빌더에서 확인할 수 있습니다. |
| `message` | string | 조건부 | - | 이번 턴의 사용자 입력입니다. 생략하거나 공백이면 history 또는 messages의 마지막 비어 있지 않은 user 메시지를 사용합니다. 대체할 user 메시지가 없으면 E4201/400; 최대 4,000자 |
| `session_id` | string | 선택 | - | 운영 로그에서 같은 상담을 묶을 고객 측 세션 ID입니다. 영문, 숫자, `.`, `_`, `:`, `-`만 사용; 1–160자 |
| `history` | Turn[] | 선택 | [] | 최근 대화입니다. 각 항목은 role과 content를 가지며 messages보다 우선합니다. 마지막 16턴 사용; role은 user 또는 assistant; content는 턴당 최대 4,000자 |
| `messages` | Turn[] | 선택 | [] | history와 같은 형식의 별칭입니다. history가 없을 때만 사용됩니다. 마지막 16턴 사용 |
| `state` | object \| null | 선택 | null | 이전 응답의 state를 그대로 전달하면 멀티턴 상태가 이어집니다. JSON 문자열 기준 최대 16,000자 |
| `stream` | boolean | 선택 | false | true면 Server-Sent Events로 응답합니다. Accept 헤더로도 활성화할 수 있습니다. |

### 요청 예시

```json
{
  "agent_id": "00000000-0000-0000-0000-000000000000",
  "message": "예약 취소 수수료가 어떻게 되나요?",
  "session_id": "customer-42:chat-7",
  "history": [],
  "state": null,
  "stream": false
}
```

### 응답

#### 200 · JSON 응답

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `agent_id` | UUID | 필수 | - | 응답한 에이전트 ID입니다. |
| `agent_name` | string | 필수 | - | 에이전트 이름입니다. |
| `response` | string | 필수 | - | 최종 답변입니다. |
| `matched_intent` | string \| null | 필수 | - | 매칭된 의도 이름입니다. |
| `handoff_required` | boolean | 필수 | - | 상담원 연결이 필요한지 나타냅니다. |
| `retrieved_context` | object[] | 필수 | - | 답변에 사용한 지식 항목입니다. |
| `actions` | object[] | 필수 | - | 실행한 에이전트 액션입니다. |
| `state` | object | 필수 | - | 다음 요청에 다시 보낼 멀티턴 상태입니다. |
| `end_call` | boolean | 필수 | - | 음성 채널에서 통화 종료가 필요한지 나타냅니다. |
| `runtime` | object | 필수 | - | 응답 채널 메타데이터입니다. |

```json
{
  "agent_id": "00000000-0000-0000-0000-000000000000",
  "agent_name": "라온투어 고객센터",
  "response": "예약 조건에 따라 취소 수수료가 달라질 수 있습니다.",
  "matched_intent": "패키지 취소료 기준",
  "handoff_required": false,
  "retrieved_context": [],
  "actions": [],
  "state": { "topic": "cancellation_fee", "handoff": false },
  "end_call": false,
  "runtime": { "mode": "text", "voice_engine": "DIVE" }
}
```

#### 200 · SSE 응답

stream이 true이거나 Accept가 text/event-stream이면 start, chunk, 선택적인 tool_call, done 순서로 전송됩니다. 실패 시 error 이벤트로 종료됩니다.

```http
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
X-Accel-Buffering: no
```

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `start` | event | 필수 | - | 의도, 핸드오프, 검색 지식, 초기 state입니다. |
| `chunk` | event | 필수 | - | 화면에 이어 붙일 delta 문자열입니다. |
| `tool_call` | event | 선택 | - | 도구 실행이 공개될 때의 이름과 인자입니다. |
| `done` | event | 필수 | - | 사용량 정산까지 성공한 뒤 보내는 최종 response, actions, state, end_call입니다. |
| `error` | event | 조건부 | - | 스트림 처리 실패 시 오류 메시지입니다. |

#### 400 · E4201 · 잘못된 요청

agent_id가 없거나, message와 history/messages의 user 입력이 모두 비어 있거나, 필드 형식이 잘못된 경우입니다. 사용량 예약·과금·답변 생성 전에 거절됩니다.

```json
{
  "error": "message or a user message in history is required",
  "code": "E4201"
}
```

#### 401 · E4001 / E4002 · 인증 오류

Bearer API Key가 없거나 유효하지 않습니다.

#### 402 · E4601 · 크레딧 부족

최대 예상 사용량을 예약할 크레딧이 부족합니다.

#### 404 · E4301 · 에이전트 없음

API Key 조직에서 agent_id를 찾을 수 없습니다.

#### 409 · E4302 · 게시 버전 없음

에이전트에 사용할 수 있는 게시 버전이 없습니다.

#### 413 · E4204 · 요청 크기 초과

요청 body 또는 state가 허용 크기를 초과했습니다.

#### 429 · E4403 · 호출 제한 초과

API Key 기준 분당 600회 제한을 초과했습니다.

#### 500 / 503 · E5001 / E5002 / E5003 · 서버 오류, 일시적 서비스 불가, 처리 제한 시간 초과

의존 서비스 불가·과부하·사용할 수 없는 응답은 E5002/503이며 backoff 후 제한적으로 재시도할 수 있습니다. 턴 처리 제한 시간 초과는 E5003/500, 그 밖의 예상치 못한 오류는 E5001/500입니다. 실패한 요청의 최대 예약분은 정산 전에 환급되며, SSE는 정산 실패 시 done 대신 error 이벤트로 종료됩니다.

### 주의사항

- 요청 body는 최대 64,000 bytes이며 API Key별 호출 제한은 분당 600회입니다.
- message와 각 history content는 앞뒤 공백을 제거한 뒤 최대 길이까지만 사용합니다.
- message와 history/messages의 user 입력이 모두 비어 있으면 E4201/400이며 사용량 예약, 답변 생성, 상담 기록이 발생하지 않습니다.
- session_id가 허용 패턴과 맞지 않으면 요청은 처리되지만 세션 ID는 기록되지 않습니다.

## 상담 목록

에이전트의 전화·웹 위젯·API 상담을 시작 시각 최신순으로 조회합니다. 통계는 이 목록과 요약 값으로 직접 만듭니다. 테스트 대화는 포함하지 않습니다. '상담 조회 허용'을 켠 API Key만 부를 수 있습니다.

```http
GET https://console.humelo.com/api/v1/agents/{agent_id}/consultations
```

### 인증 및 헤더

```http
Authorization: Bearer {YOUR_API_KEY}
```

### 경로 파라미터

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `agent_id` | UUID | 필수 | - | API Key 조직의 에이전트 ID입니다. |

### Query

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `from` | RFC 3339 | 선택 | to의 30일 전 | 이 시각 이후 시작한 상담부터 조회합니다. |
| `to` | RFC 3339 | 선택 | 지금 | 이 시각 전에 시작한 상담까지 조회합니다. from과 최대 92일 |
| `channel` | string | 선택 | - | phone, web_widget, api 중 하나로 거릅니다. |
| `status` | string | 선택 | - | ai_handling, handoff_requested, agent_active, resolved 중 하나로 거릅니다. |
| `limit` | integer | 선택 | 20 | 한 번에 받을 상담 수입니다. 1~100 |
| `cursor` | string | 선택 | - | 이전 응답의 next_cursor입니다. 같은 기간·조건으로 다음 쪽을 받습니다. |

### 요청 예시

```bash
curl "https://console.humelo.com/api/v1/agents/{agent_id}/consultations?from=2026-10-01T00:00:00%2B09:00&channel=phone&limit=50" \
  -H "Authorization: Bearer $HUMELO_API_KEY"
```

### 응답

#### 200 · 목록 응답

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `consultations` | object[] | 필수 | - | 상담 목록입니다. 항목 필드는 아래 상담 필드와 같습니다. |
| `next_cursor` | string \| null | 필수 | - | 다음 쪽이 있으면 cursor로 보낼 값, 없으면 null입니다. |
| `id` | UUID | 필수 | - | 상담 ID입니다. 상세 조회에 씁니다. |
| `channel` | string | 필수 | - | phone, web_widget, api(카카오톡 채널 포함) 중 하나입니다. |
| `status` | string | 필수 | - | ai_handling, handoff_requested, agent_active, resolved 중 하나입니다. |
| `title` | string \| null | 필수 | - | 상담 제목입니다. |
| `started_at` | ISO 8601 | 필수 | - | 상담 시작 시각입니다. 목록 기간과 정렬의 기준입니다. |
| `last_message_at` | ISO 8601 | 필수 | - | 마지막 메시지 시각입니다. |
| `handoff_requested` | boolean | 필수 | - | 상담원 연결이 필요했던 상담인지 나타냅니다. |
| `purged` | boolean | 필수 | - | 보관 기간이 지나 내용을 파기한 상담이면 true입니다. 이때 title·summary·발신번호·대화·접수는 비어 있습니다. |
| `call` | object | 조건부 | - | 전화 상담의 caller_number, started_at, ended_at, duration_seconds입니다. channel=phone일 때만 |
| `summary` | object \| null | 필수 | - | 상담 요약입니다. 웹훅 conversation.summary의 data와 같은 모양(summary, customer_intent, resolution_status, quality_score, quality_reason, system, custom_fields 등)이며 요약이 아직 없으면 null입니다. |

```json
{
  "consultations": [
    {
      "id": "6f1c2a4e-0d7b-4c51-9a3e-2b9f8c7d6e5a",
      "channel": "phone",
      "status": "resolved",
      "title": "충전 금액 문의",
      "started_at": "2026-10-29T01:12:03.512Z",
      "last_message_at": "2026-10-29T01:14:40.118Z",
      "handoff_requested": false,
      "purged": false,
      "call": {
        "caller_number": "01012345678",
        "started_at": "2026-10-29T01:12:01Z",
        "ended_at": "2026-10-29T01:14:45Z",
        "duration_seconds": 164
      },
      "summary": {
        "summary": "고객이 최근 충전 금액을 문의해 조회 결과를 안내했습니다.",
        "customer_intent": "충전 금액 문의",
        "resolution_status": "resolved",
        "quality_score": 9
      }
    }
  ],
  "next_cursor": null
}
```

#### 400 · E4201 · 잘못된 요청

기간·채널·상태·limit·cursor 값이 형식에 맞지 않습니다.

#### 401 · E4001 / E4002 · 인증 오류

Bearer API Key가 없거나 유효하지 않습니다.

#### 403 · E4101 · 권한 없음

API Key에 '상담 조회 허용'이 꺼져 있거나 공공 구성 조직의 키입니다. 상담 조회 허용은 API 키 화면에서 조직 대표가 키마다 켭니다. 공공 구성 조직은 외부 도구 연결과 같이 상담 기록을 API로 가져갈 수 없습니다.

#### 404 · E4301 · 에이전트 또는 상담 없음

API Key 조직에서 에이전트를 찾을 수 없거나, 그 에이전트의 상담이 아닙니다.

#### 429 · E4403 · 호출 제한 초과

API Key 기준 조회 분당 600회 제한을 초과했습니다. 대화 API와 따로 셉니다.

#### 503 · E5002 · 일시적 서비스 불가

상담 기록을 일시적으로 읽을 수 없습니다. backoff 후 다시 시도하세요.

### 주의사항

- 발신번호·대화 내용·접수 정보처럼 개인정보가 담긴 값을 돌려주므로 API Key는 서버에만 보관하세요.
- 보관 기간(1년)이 지난 상담은 purged=true로 남고 내용은 비어 있습니다.
- 목록·상세 조회는 모두 API Key·조회 조건·건수 또는 상담 ID로 접속기록에 남습니다(발신번호·대화 내용은 기록하지 않습니다).
- 통화가 상담 대화 없이 끝난 경우(첫 응답 전 종료 등)는 목록에 없습니다.

## 상담 상세

상담 하나와 고객·AI·상담원 대화, 고객 요청 접수를 조회합니다. 내부 메모와 시스템 이벤트는 포함하지 않습니다.

```http
GET https://console.humelo.com/api/v1/agents/{agent_id}/consultations/{consultation_id}
```

### 인증 및 헤더

```http
Authorization: Bearer {YOUR_API_KEY}
```

### 경로 파라미터

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `agent_id` | UUID | 필수 | - | API Key 조직의 에이전트 ID입니다. |
| `consultation_id` | UUID | 필수 | - | 목록의 상담 ID입니다. |

### 응답

#### 200 · 상세 응답

| 필드 | 타입 | 필수 여부 | 기본값 | 설명 |
| --- | --- | --- | --- | --- |
| `consultation` | object | 필수 | - | 목록 항목과 같은 상담 필드입니다. |
| `messages` | object[] | 필수 | - | role(customer 고객, ai AI, counselor 상담원, notice 고객이 본 상담원 연결 안내), content, created_at을 시각순으로 담습니다. |
| `receptions` | object[] | 필수 | - | 고객 요청 접수의 id, status, reason, fields(접수한 정보), requested_at, resolved_at입니다. |

```json
{
  "consultation": {
    "id": "6f1c2a4e-0d7b-4c51-9a3e-2b9f8c7d6e5a",
    "channel": "phone",
    "status": "resolved",
    "title": "충전 금액 문의",
    "started_at": "2026-10-29T01:12:03.512Z",
    "last_message_at": "2026-10-29T01:14:40.118Z",
    "handoff_requested": false,
    "purged": false,
    "call": {
      "caller_number": "01012345678",
      "started_at": "2026-10-29T01:12:01Z",
      "ended_at": "2026-10-29T01:14:45Z",
      "duration_seconds": 164
    },
    "summary": {
      "summary": "고객이 최근 충전 금액을 문의해 조회 결과를 안내했습니다.",
      "customer_intent": "충전 금액 문의",
      "resolution_status": "resolved",
      "quality_score": 9
    }
  },
  "messages": [
    { "role": "customer", "content": "최근 충전 금액 알려 주세요.", "created_at": "2026-10-29T01:12:05.201+00:00" },
    { "role": "ai", "content": "최근 충전 금액은 3만 원입니다.", "created_at": "2026-10-29T01:12:09.880+00:00" }
  ],
  "receptions": []
}
```

#### 400 · E4201 · 잘못된 요청

기간·채널·상태·limit·cursor 값이 형식에 맞지 않습니다.

#### 401 · E4001 / E4002 · 인증 오류

Bearer API Key가 없거나 유효하지 않습니다.

#### 403 · E4101 · 권한 없음

API Key에 '상담 조회 허용'이 꺼져 있거나 공공 구성 조직의 키입니다. 상담 조회 허용은 API 키 화면에서 조직 대표가 키마다 켭니다. 공공 구성 조직은 외부 도구 연결과 같이 상담 기록을 API로 가져갈 수 없습니다.

#### 404 · E4301 · 에이전트 또는 상담 없음

API Key 조직에서 에이전트를 찾을 수 없거나, 그 에이전트의 상담이 아닙니다.

#### 429 · E4403 · 호출 제한 초과

API Key 기준 조회 분당 600회 제한을 초과했습니다. 대화 API와 따로 셉니다.

#### 503 · E5002 · 일시적 서비스 불가

상담 기록을 일시적으로 읽을 수 없습니다. backoff 후 다시 시도하세요.

## Next.js Server Proxy

Use this shape when you want a ChannelTalk-style widget on your website. The browser talks to your backend. Your backend calls Prosody.

```ts
// app/api/agent/chat/route.ts
export async function POST(request: Request) {
  const body = await request.json();

  const response = await fetch("https://console.humelo.com/api/v1/agents/chat", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.PROSODY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      agent_id: process.env.PROSODY_AGENT_ID,
      message: body.message,
      history: body.history ?? [],
      state: body.state ?? null,
      stream: false,
    }),
  });

  return new Response(await response.text(), {
    status: response.status,
    headers: { "Content-Type": response.headers.get("Content-Type") ?? "application/json" },
  });
}
```

## Browser Widget Pattern

A production widget usually needs four pieces:

1. A launcher button fixed to the bottom corner.
2. A chat panel that stores local draft messages.
3. A server-side proxy endpoint.
4. A small session store that preserves `history` and Prosody `state`.

Minimal client shape:

```ts
type Turn = { role: "user" | "assistant"; content: string };

let history: Turn[] = [];
let state: Record<string, unknown> | null = null;

async function sendToAgent(message: string) {
  const response = await fetch("/api/agent/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message, history, state }),
  });

  if (!response.ok) {
    throw new Error("Agent is temporarily unavailable");
  }

  const result = await response.json();
  history = [
    ...history,
    { role: "user", content: message },
    { role: "assistant", content: result.response },
  ].slice(-16);
  state = result.state ?? state;
  return result;
}
```

## Multi-Turn Rules

Prosody does not require you to resend the full conversation forever. Send a short recent history window and the returned `state`. The state is designed to preserve compact task context such as current topic, handoff flow, and useful runtime hints.

Recommended defaults:

- Keep the last 8 to 16 turns in `history`.
- Persist `state` per user conversation.
- Start a new state when the user intentionally starts a new support session.
- Do not include sensitive internal notes that should not be used by the agent.

## Handoff Handling

If `handoff_required` is `true`, show a human-support path. You can still display `response`; it is written to explain what information is needed before escalation.

```ts
if (result.handoff_required) {
  openHumanSupport({
    transcript: history,
    state: result.state,
    matchedIntent: result.matched_intent,
  });
}
```

## Consultation Records

Read the agent's consultations to build your own statistics or archive. The API key needs **Allow consultation reads** (Settings → API Keys, set by the organization owner when creating or editing the key); keys without it are refused with `E4101`. Turn it on only for the key your integration server uses.

```http
GET /api/v1/agents/{agent_id}/consultations?from=2026-10-01T00:00:00%2B09:00&to=2026-10-29T00:00:00%2B09:00&channel=phone&limit=50
GET /api/v1/agents/{agent_id}/consultations/{consultation_id}
```

- The list returns phone, web widget, and API (including KakaoTalk channel) consultations started in `[from, to)`, newest first. `from` defaults to 30 days before `to` (default: now); the window is at most 92 days. Filter with `channel` (`phone`, `web_widget`, `api`) and `status` (`ai_handling`, `handoff_requested`, `agent_active`, `resolved`); `limit` is 1-100 (default 20). Pass `next_cursor` back as `cursor` with the same filters for the next page; it is `null` on the last page. Test conversations are never listed.
- Each consultation has `id`, `channel`, `status`, `title`, `started_at`, `last_message_at`, `handoff_requested`, `purged`, phone calls a `call` object (`caller_number`, `started_at`, `ended_at`, `duration_seconds`), and `summary`: the same object as the `conversation.summary` webhook data (`summary`, `customer_intent`, `resolution_status`, `quality_score`, `quality_reason`, `system`, `custom_fields`, ...), or `null` before the summary is ready.
- The detail adds `messages` (`role` is `customer`, `ai`, `counselor`, or `notice` for a counselor notice the customer saw, such as a takeover; internal notes and system events are left out) and `receptions` (submitted customer requests with their collected `fields`).
- Consultations older than the one-year retention are kept as `purged: true` with their contents emptied.
- The responses carry caller numbers and conversation text: keep the key on your server. Reads have their own limit of 600 requests per minute per key, separate from chat calls.
- Every read is recorded in Prosody's access log with the key, the filters and row count or the consultation id (never the caller number or the text). Keys of public-sector organizations are refused with `E4101` even with the permission, as their external tool connections are.

## Security Checklist

- Keep `PROSODY_API_KEY` on the server only.
- Restrict your proxy with your own session, origin, or rate limit policy.
- Never put the API key in a script tag, public environment variable, or mobile bundle.
- Log Prosody request IDs or your own correlation IDs for support.
- Redact PII before storing transcripts if your policy requires it.

## Error Handling

| Status | Meaning | Recommended handling |
| --- | --- | --- |
| 400 | Missing or malformed request | Fix request shape before retrying. |
| 401 | Invalid API key | Rotate or reconfigure the key. |
| 402 | Insufficient credits | Add credits before retrying. |
| 404 | AICC not found or not owned by the API key organization | Check `agent_id` and organization ownership. |
| 409 | No published agent version | Publish a version before retrying. |
| 413 | Request body or state too large | Reduce the request size. |
| 429 | Per-key rate limit exceeded | Back off until the next rate-limit window. |
| 500 | Internal error | Show a fallback and do not blindly retry. |
| 503 | Runtime temporarily unavailable | Retry with bounded exponential backoff. |

AICC errors are flat, for example `{"error":"Too many requests","code":"E4403"}`. For SSE, a successfully settled turn ends with `done`; a runtime or settlement failure ends with an `error` event and no `done` event.

## LLM/Agent Context

Use the following files when asking ChatGPT, Claude, Codex, or another coding agent to integrate Prosody AICC:

- `/llms.txt`: documentation index.
- `/llms-full.txt`: all available documentation in one text file.
- `/docs-md/aicc`: this page as Markdown.

Prompt example:

```txt
Use the Prosody AICC documentation from /docs-md/aicc.
Implement a server-side proxy and a browser chat widget.
Do not expose PROSODY_API_KEY to the browser.
```
