# 관리번호 발급 링크 (/docs/api-reference/assignment-links)

# 관리번호 발급 링크

엔드유저가 방문하여 번호를 직접 발급받을 수 있는 일회용 링크를 생성합니다. 링크를 통해 발급된 번호는 발급자(엔드유저)의 KYC 정보와 함께 계정에 등록되며, 이후 통화/메시지가 지정한 webhook으로 전달됩니다.

이 기능은 **관리번호 추가** 애드온이 활성화된 계정에서만 **생성/취소(POST/DELETE)** 가 가능합니다. 대시보드 **부가서비스 → 관리번호 추가** 에서 계정 소유자가 켤 수 있습니다. Business 플랜과 사업자 인증이 필요합니다. 조회(GET) 엔드포인트는 애드온 활성 여부와 무관하게 사용할 수 있습니다.

## 동작 방식

1. **링크 생성 (POST)** — `webhookUrl`/`webhookMethod`/`note`/`ttlSeconds`/`returnUrl`을 옵션으로 지정해 일회용 링크를 만듭니다. 응답으로 받은 `url`을 엔드유저에게 전달합니다.
2. **엔드유저 방문 + KYC** — 엔드유저가 링크를 열고 본인인증을 완료하면 번호가 발급되고 링크는 `consumed` 상태로 전환됩니다. 법인 가입자는 대표자 본인이 본인인증해야 발급됩니다. 이 시점에 [`assignment.completed` Webhook](/docs/webhooks/assignment) 이 전송되므로, 발급 결과를 알기 위해 폴링할 필요가 없습니다.
3. **번호 사용** — 발급된 번호로 인입되는 통화/메시지가 생성 시 지정한 `webhookUrl`로 전송됩니다.
4. **취소 (DELETE)** — 아직 사용되지 않은 `pending` 링크를 취소할 수 있습니다.

한 링크는 1회만 소비되며, `consumed` / `expired` / `revoked` 상태가 된 후에는 재사용할 수 없습니다.

## 회선 발급

관리번호 발급은 **Business 플랜 전용** 기능입니다. 10회선까지 무료이고, 11회선이 되는 날부터 월 ₩990,000입니다([요금](/docs/api-reference/pricing)). 같은 사람은 한 계정에서 관리번호를 1개만 받을 수 있습니다.

## 링크 상태

| 상태 | 의미 |
|------|------|
| `pending` | 발급 대기. 엔드유저가 아직 방문하지 않은 상태. |
| `consumed` | 엔드유저가 KYC를 완료하여 번호가 발급된 상태. |
| `expired` | TTL이 지나 자동 만료된 상태. |
| `revoked` | 발급 측에서 명시적으로 취소한 상태. |

## 엔드포인트

| Method | Path | 설명 |
|--------|------|------|
| `POST` | `/v1/accounts/{accountId}/assignment-links` | 링크 생성 (애드온 필요) |
| `GET` | `/v1/accounts/{accountId}/assignment-links` | 링크 목록 조회 |
| `GET` | `/v1/accounts/{accountId}/assignment-links/{linkId}` | 링크 단건 조회 |
| `DELETE` | `/v1/accounts/{accountId}/assignment-links/{linkId}` | pending 링크 취소 (애드온 필요) |

## 링크 생성

```bash
curl -X POST "https://api.claw-ops.com/v1/accounts/AC.../assignment-links" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://my-app.com/voice",
    "webhookMethod": "POST",
    "note": "신규 가입자용",
    "ttlSeconds": 172800,
    "returnUrl": "https://my-app.com/onboarding/done"
  }'
```

응답 (201):

```json
{
  "token": "FUjxM9qBBy_EIZp7QizzWSqXyUqqmrsnviw7PLdDI5w",
  "url": "https://platform.claw-ops.com/assign/FUjxM9qBBy_EIZp7QizzWSqXyUqqmrsnviw7PLdDI5w",
  "expiresAt": "2026-05-04T10:23:51.733Z"
}
```

`url`을 그대로 엔드유저에게 전달하세요. `token`은 이후 단건 조회/취소 시 `linkId`로 사용합니다.

### 요청 본문

| 이름 | 타입 | 기본 | 설명 |
|------|------|------|------|
| `webhookUrl` | string | — | 발급된 번호로 인입되는 통화/메시지를 받을 주소 |
| `webhookMethod` | enum | `POST` | `POST` / `GET` |
| `webhookHeaders` | object | — | 발급 시 번호에 prefill 될 헤더 |
| `note` | string | — | 내부 메모 (200자). `assignment.completed` 로 되돌아옵니다 |
| `ttlSeconds` | integer | 7일 | 링크 만료까지의 시간(초). `60` ~ `604800` |
| `returnUrl` | string | — | 발급 완료 후 엔드유저를 되돌려 보낼 주소 |

`returnUrl` 의 호스트는 **미리 등록한 도메인과 정확히 일치**해야 합니다. 대시보드 **설정 → 조직 → 발급 페이지** 에서 등록하며, 등록하지 않은 도메인을 넣으면 400 을 반환합니다. 서브도메인은 자동으로 허용되지 않으니 쓰시는 호스트를 각각 등록해 주세요.

발급이 끝나면 엔드유저에게 「돌아가기」 버튼이 표시되고, 잠시 뒤 이 주소로 자동 이동합니다. 발급된 번호는 주소에 붙이지 않습니다 — 그 값은 [`assignment.completed` Webhook](/docs/webhooks/assignment) 으로 받으세요.

## 목록 조회

```bash
curl "https://api.claw-ops.com/v1/accounts/AC.../assignment-links?status=pending&page=0&pageSize=20" \
  -H "Authorization: Bearer sk_..."
```

응답:

```json
{
  "data": [
    {
      "linkId": "FUjxM9qBBy_EIZp7QizzWSqXyUqqmrsnviw7PLdDI5w",
      "url": "https://platform.claw-ops.com/assign/FUjxM9qBBy_EIZp7QizzWSqXyUqqmrsnviw7PLdDI5w",
      "status": "pending",
      "createdAt": "2026-04-27T10:23:51.733Z",
      "expiresAt": "2026-05-04T10:23:51.733Z",
      "consumedAt": null,
      "webhookUrl": "https://my-app.com/voice",
      "webhookMethod": "POST",
      "note": "신규 가입자용",
      "returnUrl": "https://my-app.com/onboarding/done",
      "assignment": null
    }
  ],
  "meta": { "page": 0, "pageSize": 20, "total": 1 }
}
```

쿼리 파라미터:

| 이름 | 타입 | 기본 | 설명 |
|------|------|------|------|
| `status` | enum | — | `pending` / `consumed` / `expired` / `revoked` |
| `page` | integer | 0 | 페이지 번호 (0부터) |
| `pageSize` | integer | 20 | 페이지 크기 (1~100) |

## 단건 조회

```bash
curl "https://api.claw-ops.com/v1/accounts/AC.../assignment-links/{linkId}" \
  -H "Authorization: Bearer sk_..."
```

`consumed` 상태 링크는 `assignment` 필드에 발급된 번호 정보가 포함됩니다:

```json
{
  "linkId": "...",
  "status": "consumed",
  "consumedAt": "2026-04-28T03:12:00.000Z",
  "assignment": {
    "number": "07012340001",
    "name": "홍길동",
    "consumedAt": "2026-04-28T03:12:00.000Z",
    "releasedAt": null
  }
}
```

`releasedAt`이 `null`이 아니면 해당 번호는 이미 반납된 상태입니다.

## 링크 취소

`pending` 상태 링크만 취소할 수 있습니다. 이미 사용/만료/취소된 링크에 호출하면 409 `NOT_PENDING`을 반환합니다.

```bash
curl -X DELETE "https://api.claw-ops.com/v1/accounts/AC.../assignment-links/{linkId}" \
  -H "Authorization: Bearer sk_..."
```

## 에러 코드

| HTTP | code | 설명 |
|------|------|------|
| 400 | — | 입력 검증 실패 (URL 형식, `pageSize` 정수 아님 등) |
| 401 | — | 인증 실패 / 누락 |
| 403 | `FEATURE_DISABLED` | **관리번호 추가** 애드온이 비활성. POST/DELETE에서만 발생, GET은 영향 없음 |
| 403 | `BUSINESS_VERIFICATION_REQUIRED` | 사업자 인증을 마친 조직이 아님. POST에서만 발생 |
| 403 | — | API 키가 path의 `accountId` 와 다른 계정에 속함 |
| 404 | `ACCOUNT_NOT_FOUND` | 계정을 찾을 수 없음 (POST) |
| 404 | — | 링크를 찾을 수 없음 (GET 단건) |
| 409 | `NOT_PENDING` | 취소할 수 없는 상태 (이미 사용/만료/취소됨) |
| 422 | `NO_SUBSCRIPTION` | 활성 구독이 없음 |
| 422 | `QUOTA_EXCEEDED` | 회선 발급 한도 초과 (Business 플랜에서는 무제한+metered 과금이라 통상 발생하지 않음) |

## SDK 예시

### Python

```python
from clawops import ClawOps

client = ClawOps(api_key="sk_...", account_id="AC...")

# 생성
link = client.assignment_links.create(
    webhook_url="https://my-app.com/voice",
    note="신규 가입자용",
)
print(link.url)

# 목록 (자동 페이지네이션)
for item in client.assignment_links.list(status="pending").auto_paging_iter():
    print(item.link_id, item.status)

# 단건
detail = client.assignment_links.retrieve(link.token)

# 취소
client.assignment_links.revoke(link.token)
```

### Node

```ts
import ClawOps from 'clawops';

const client = new ClawOps({ apiKey: 'sk_...', accountId: 'AC...' });

const link = await client.assignmentLinks.create({
  webhookUrl: 'https://my-app.com/voice',
  note: '신규 가입자용',
});

const page = await client.assignmentLinks.list({ status: 'pending' });
for (const item of page.data) {
  console.log(item.linkId, item.status);
}

const detail = await client.assignmentLinks.retrieve(link.token);
await client.assignmentLinks.revoke(link.token);
```
