# Convert API

> 가맹점 잔액에서 직접 암호화폐를 환전하세요 — 실시간 견적을 받아 시장 가격으로 실행합니다.

Convert API를 사용하면 가맹점 잔액에 보유한 통화 간에 현재 시장 가격으로 환전할 수 있습니다 — 가맹점 대시보드의 **환전(Swap)** 탭을 구동하는 것과 동일한 엔진을 이제 백엔드에서 직접 호출할 수 있습니다.

> **WARNING:** Convert 엔드포인트는 [Payment API](/docs/payments) 요청에 사용하는 것과 동일한 **일반 API 키**로 서명합니다 — Payout API 키가 **아닙니다**. 환전을 실행하면 가맹점 잔액이 즉시 차감 및 적립되므로, 자금을 이동시키는 다른 자격 증명과 동일한 수준의 주의를 기울여 이 키를 관리하세요.

## 환전 견적 조회

현재 시장 가격 기준의 참고용 견적(유효 환율 및 결과 금액)을 반환합니다. 아무것도 차감되거나 예약되지 않으므로 실행 전 필요한 만큼 호출할 수 있습니다.

`POST /v1/convert/price`

### 요청 파라미터

| 필드 | 타입 | 필수 | 설명 | 값 |
|------|------|------|------|-----|
| `from_currency` | string | 예 | 환전 대상 통화 | `BTC`, `ETH`, `USDT`, `USDC`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `to_currency` | string | 예 | 환전 목표 통화. `from_currency`와 달라야 함 | `USDT`, `USDC`, `BTC`, `ETH`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `amount` | decimal | 예 | 환전할 금액, `0`보다 커야 함 |  |
| `amount_type` | string | 예 | `amount`가 가리키는 쪽 | `from`, `to` |

> **INFO:** `amount_type=from`은 `from_currency`를 정확히 `amount`만큼 지출합니다. `amount_type=to`는 `to_currency`를 정확히 `amount`만큼 수령합니다.

**🟢 200 OK** · `application/json`

```json
{
  "state": 0,
  "result": {
    "success": true,
    "from_currency": "BTC",
    "to_currency": "USDT",
    "amount_type": "from",
    "from_amount": "0.01000000",
    "to_amount": "947.86690000",
    "effective_rate": "94786.69000000",
    "from_amount_usd": "947.87",
    "to_amount_usd": "947.87"
  }
}
```

#### 응답 필드

| 필드 | 타입 | 설명 |
|------|------|------|
| `success` | boolean | 견적이 성공적으로 계산되었는지 여부 |
| `from_currency` | string | 대상 통화 |
| `to_currency` | string | 목표 통화 |
| `amount_type` | string | 요청의 `amount_type`을 그대로 반영 |
| `from_amount` | string | `from_currency`로 차감될 금액 |
| `to_amount` | string | `to_currency`로 적립될 금액 |
| `effective_rate` | string | 이 견적에 적용된 환율 — `from_currency` 1단위당 `to_currency` 수량(이미 플랫폼 가격 정책 반영) |
| `from_amount_usd` | string \| null | `from_amount`의 USD 환산액 |
| `to_amount_usd` | string \| null | `to_amount`의 USD 환산액 |

- 이 견적은 **참고용일 뿐**입니다 — 견적 조회와 실행 호출 사이에 시장 가격이 변동될 수 있습니다.
- 이 호출은 잔액을 차감하거나 예약하지 않습니다.

> Use your project UUID and the endpoint-appropriate API key from the merchant dashboard.

#### Interactive request: `POST /v1/convert/price`
  - `from_currency` (enum, required): BTC,ETH,USDT,USDC,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `to_currency` (enum, required): USDT,USDC,BTC,ETH,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `amount` (decimal, required)
  - `amount_type` (enum, required): from,to

## 환전 실행

현재 시장 가격으로 환전을 실행하고 가맹점 잔액을 업데이트합니다. 별도의 "견적 확정" 단계는 없습니다 — 환전하려는 금액으로 이 엔드포인트를 직접 호출하세요.

`POST /v1/convert`

> **INFO:** **멱등성(Idempotency).** 첫 호출 후 약 1분 이내에 완전히 동일한 요청(동일한 `from_currency`, `to_currency`, `amount`, `amount_type`)을 반복하면 새 환전을 생성하는 대신 기존 환전 결과를 반환합니다. 이 시간이 지나면 동일한 요청도 새로운 환전으로 처리됩니다 — 타임아웃 발생 시 이전 호출 결과를 먼저 확인하지 않고 무작정 재시도하지 마세요.

> **WARNING:** 이 엔드포인트는 호출자당 **분당 10회 요청**으로 제한됩니다 — 매 호출이 실제 잔액을 움직이기 때문에 일반 API 속도 제한보다 더 엄격합니다.

### 요청 파라미터

| 필드 | 타입 | 필수 | 설명 | 값 |
|------|------|------|------|-----|
| `from_currency` | string | 예 | 환전 대상 통화 | `BTC`, `ETH`, `USDT`, `USDC`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `to_currency` | string | 예 | 환전 목표 통화. `from_currency`와 달라야 함 | `USDT`, `USDC`, `BTC`, `ETH`, `TRX`, `BNB`, `GRAM`, `SOL`, `POL`, `LTC`, `DASH`, `DOGE`, `ZEC`, `XRP`, `XMR` |
| `amount` | decimal | 예 | 환전할 금액, `0`보다 커야 함 |  |
| `amount_type` | string | 예 | `amount`가 가리키는 쪽 | `from`, `to` |

**🟢 200 OK** · `application/json`

```json
{
  "state": 0,
  "result": {
    "id": 12345,
    "type": "manual",
    "status": "completed",
    "from_currency": "BTC",
    "to_currency": "USDT",
    "from_amount": "0.01000000",
    "requested_from_amount": "0.01000000",
    "refund_amount": null,
    "to_amount": "947.86690000",
    "exchange_rate": "94786.69000000",
    "fee_amount": "0.00000000",
    "from_amount_usd": "947.87",
    "to_amount_usd": "947.87",
    "processed_at": "2026-01-20T15:30:24Z",
    "created_at": "2026-01-20T15:30:22Z"
  }
}
```

#### 응답 필드

| 필드 | 타입 | 설명 |
|------|------|------|
| `id` | int | 시스템이 부여한 환전 주문 ID |
| `type` | string | 이 API에서는 항상 `manual` |
| `status` | string | 현재 상태 (아래 «환전 상태» 참고) |
| `from_currency` | string | 대상 통화 |
| `to_currency` | string | 목표 통화 |
| `from_amount` | string | `from_currency`로 차감된 금액 |
| `requested_from_amount` | string \| null | `amount_type = from`일 때 원래 요청한 대상 금액. `amount_type = to`일 때는 `null` |
| `refund_amount` | string \| null | 부분 체결 후 환불된 사전 차감 금액의 일부. 주문이 완전히 체결되면 `null` |
| `to_amount` | string | `to_currency`로 적립된 금액 |
| `exchange_rate` | string | 이 환전에 실제 적용된 환율 — `from_currency` 1단위당 `to_currency` 수량(이미 플랫폼 가격 정책 반영) |
| `fee_amount` | string | 이 환전에 부과된 플랫폼 수수료로, 거래 방향에 따라 `from_currency` 또는 `to_currency`로 표시됨. 이미 `exchange_rate`에 반영되어 있으며 투명성을 위해 표시됨 |
| `from_amount_usd` | string \| null | `from_amount`의 USD 환산액 |
| `to_amount_usd` | string \| null | `to_amount`의 USD 환산액 |
| `processed_at` | string (ISO 8601) \| null | 환전 실행이 완료된 시각. 처리 중일 때는 `null` |
| `created_at` | string (ISO 8601) | 환전 주문이 생성된 시각 |

#### 환전 상태

| 상태 | 설명 |
|------|------|
| `pending` | 생성됨, 아직 시장에 전송되지 않음 |
| `processing` | 잔액이 잠기고 주문이 시장에 등록됨 |
| `completed` | 완전히 체결됨 — `to_amount`가 잔액에 적립됨 |
| `failed` | 체결 실패 — 사전 차감된 금액이 자동으로 환불됨 |
| `partially_completed` | 직접 거래 시장이 없는 통화쌍(중간 통화를 경유하는 경로)에만 해당: 첫 번째 단계는 완료되었지만 두 번째 단계가 실패함. `to_currency` 대신 중간 통화가 적립되므로, 원래 목표를 달성하려면 그 통화로부터 다시 환전해야 함 |

#### Interactive request: `POST /v1/convert`
  - `from_currency` (enum, required): BTC,ETH,USDT,USDC,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `to_currency` (enum, required): USDT,USDC,BTC,ETH,TRX,BNB,GRAM,SOL,POL,LTC,DASH,DOGE,ZEC,XRP,XMR
  - `amount` (decimal, required)
  - `amount_type` (enum, required): from,to

## 오류

실패 시 응답에는 `state: 1`과 `error_code`가 포함됩니다 — `/v1/convert/price`와 `/v1/convert`가 공유:

**🔴 422 / 400** · `application/json`

```json
{
  "state": 1,
  "error_code": "amount_too_small",
  "errors": {
    "amount": "Amount is too small for this conversion. Please increase the amount and try again."
  }
}
```

| `error_code` | HTTP 상태 | 설명 |
|--------------|-----------|------|
| `validation_failed` | 422 | 파라미터가 유효하지 않거나 누락됨, 또는 비즈니스 규칙에 의한 거부(예: 잔액 부족) — 자세한 내용은 `errors` 필드 참고 |
| `amount_too_small` | 422 | `amount`가 이 통화쌍의 최소 거래 가능 수량보다 작음 |
| `convert_unavailable` | 400 | 지금은 환전을 실행할 수 없음(시장 데이터를 사용할 수 없거나 두 통화 간 경로가 없음) — 잠시 후 다시 시도하세요 |
| `internal_error` | 400 | 요청 처리 중 예기치 않은 서버 내부 오류 발생 |

## 들어오는 결제의 자동 변환

자동 변환은 들어오는 청구서 및 고정 지갑 크레딧에 대한 프로젝트 설정입니다. 이는 `/v1/payment`에 필드를 추가하여 구성하는 것이 아니라, 상인 대시보드에서 구성됩니다. 각 규칙은 하나 이상의 원본 통화와 목표 통화를 선택합니다.

변환이 완료되면, 결제 정보와 상인 웹훅에 다음이 포함될 수 있습니다:

```json
{
  "payment_amount": "0.14800000",
  "merchant_amount": "0.146520000000000000",
  "payer_currency": "XMR",
  "convert": {
    "to_currency": "USDT",
    "commission": "0.09000000",
    "rate": "323.21000000",
    "amount": "47.262015740000000000"
  }
}
```

금액 도메인은 의도적으로 분리되어 있습니다:

- `payment_amount` — 원본 결제 통화에서 온체인으로 감지된 금액;
- `merchant_amount` — 변환 전 상인에게 귀속되는 순 원본 금액;
- `convert.amount` — `convert.to_currency`에 크레딧된 금액;
- `convert.rate` 및 `convert.commission` — 실행된 변환 결과로, 로컬에서 다시 계산해야 하는 가격이 아닙니다.

> **WARNING:** `convert`의 부재는 의미가 있습니다: 변환이 완료되지 않았을 수 있으며, 해당 소스에 대해 구성되지 않았거나 소스 통화 크레딧으로 되돌아갔을 수 있습니다. `/exchange-rates`나 공개 시장 가격에서 목표 금액을 임의로 생성하지 마십시오.

### 자동 변환 실패 및 대체

변환은 블록체인 결제 수신 이후에 이루어집니다. 시장 가용성, 최소 주문 크기, 정밀도 제한, 거래소 시간 초과, 실행 가능한 유동성 부족 등으로 인해 변환이 지연되거나 불가능할 수 있습니다.

- 글로벌/프로젝트 최소 금액 이하의 입금은 변환 파이프라인을 우회하고 소스 통화로 크레딧됩니다.
- 일시적인 실패는 비동기적으로 재시도할 수 있습니다.
- 크거나 거래할 수 없는 예금은 재시도 정책이 소진된 후 원화 신용으로 되돌아갈 수 있습니다.
- 따라서 원하는 대상 통화 변환이 발생하지 않았더라도 결제는 유효할 수 있습니다.

통합 시 검증된 결제를 먼저 저장한 다음, 결제 정보, 선택적 `convert` 블록, 상인 잔액에서 실제로 적립된 통화를 조정해야 합니다. 자체 분석 또는 알림 시스템을 기다리는 동안 결제 웹후크의 확인을 차단하지 마십시오.

### 자동 변환 승인 테스트

적어도 다음을 테스트하십시오: 성공적인 직접 변환, 브리지/멀티 홉 변환, 최소값 이하의 먼지, 일시적 재시도, 소스 통화로의 대체, 부족지불, 초과지불, 중복 웹훅, 누락된 `convert`, 모호한 시간 초과 후의 조정.

## 수동 변환 엣지 케이스

- `/v1/convert/price`는 예시 미리보기이며, 시장 움직임에 따라 실행 결과가 변경될 수 있습니다.
- `amount_type: from`는 소스 측 요청을 수정하고, `amount_type: to`는 대상 측 금액을 요청합니다. 확인 UI를 표시할 때 의미를 바꾸지 마십시오.
- 직접 시장이 없는 페어는 중간 통화를 통해 라우팅할 수 있습니다. 한쪽만 완료되면 `partially_completed`는 중간 크레딧을 보고합니다.
- 실행 호출이 시간 초과되면 재시도하기 전에 조정하십시오. HTTP 응답이 손실되더라도 시장 주문은 실행될 수 있습니다.
- `failed`를 로컬 보상 잔액 항목을 적용할 권한으로가 아니라 조정할 상태로 취급하십시오; 플랫폼이 차변/환불 회계를 소유합니다.