> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.vpms.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 마켓플레이스 Open API

> 외부 앱용 REST Open API — OAuth2 인증, 멱등 쓰기(command outbox), 예약·결제·매핑 엔드포인트

마켓플레이스 앱이 VPMS 데이터에 접근하는 **외부 REST 평면**이다. GraphQL 과 별개의
평면이며, OAuth2 bearer 토큰으로 인증되고 설치(Installation)의 승인된 scope 로
인가된다. 내부 API 의 raw mirror 가 아닌 **public contract** 로 설계된다.

Base URL 은 **API 게이트웨이 도메인의 `/marketplace` namespace** 다
(예: `https://staging-api.vpms.io/marketplace`). 게이트웨이가 prefix 를 벗겨
marketplace-svc 로 프록시하므로, 이 문서의 모든 경로는 base URL 뒤에 그대로 붙인다
— 예: 토큰 발급은 `POST {baseUrl}/oauth/token`.

## 1. 인증 (OAuth2 client\_credentials)

앱은 설치별 access token 을 발급받는다. 표준 `client_credentials` 를 `installation_id`
파라미터로 확장한 흐름이다 — 토큰은 해당 설치의 accommodation 컨텍스트와 승인 scope 로
한정된다.

```http theme={null}
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=mp_client_xxx
&client_secret=mp_secret_xxx
&installation_id=01KXD...
```

응답:

```json theme={null}
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "reservations:write folios:write",
  "installation_id": "01KXD..."
}
```

이후 모든 호출에 `Authorization: Bearer <access_token>` 를 실는다. scope 는 설치의
`AccessGrant.effectiveScopes` 기준이며, 부족하면 `403 forbidden` 이다.

<Note>
  OAuth client(`client_id`/`client_secret`)는 앱 등록 후 콘솔에서 1회 발급된다. secret 은
  그 시점에만 노출되므로 안전하게 보관하라. `installation_id` 는 테넌트가 앱을 설치·동의한
  뒤 `installation.activated` webhook 으로 전달된다.
</Note>

## 2. 멱등성 (쓰기 필수)

모든 쓰기(`POST`/`PATCH`/`DELETE`)는 **`Idempotency-Key` 헤더가 필수**다. 없으면
`400 invalid_request`. 같은 키의 재요청은 최초 명령의 결과를 그대로 재응답하므로,
채널 매니저의 네트워크 재시도가 중복 예약/결제를 만들지 않는다.

```http theme={null}
POST /api/v1/reservations
Authorization: Bearer <token>
Idempotency-Key: your-app:resv:12345
Content-Type: application/json
```

<Warning>
  키는 **의미 있는 자연 키**를 쓰라 (예: `<app>:resv:<외부예약번호>`). 랜덤 UUID 를 매
  재시도마다 새로 만들면 멱등성이 깨진다.
</Warning>

## 3. 쓰기 모델 — command outbox (동기 확정 + WAL)

쓰기는 **hybrid** 로 처리된다: 요청을 durable 하게 journaling 한 뒤 인라인으로 내부
백엔드에 시도하고, 실패 시 워커가 at-least-once 로 재시도한다.

| 결과             |     HTTP    | 본문                                                         |
| -------------- | :---------: | ---------------------------------------------------------- |
| 인라인 성공         | `200`/`201` | 리소스 envelope + `outbox_id`                                 |
| 인라인 실패(재시도 대기) |    `202`    | `{ "accepted": true, "outbox_id": "…" }`                   |
| 검증 실패          |    `400`    | `{ "error": "invalid_request", "error_description": "…" }` |
| scope 부족       |    `403`    | —                                                          |
| 내부 백엔드 오류      |    `502`    | `{ "error": "upstream_error" }`                            |

`202` 를 받으면 최종 확정은 **지연 webhook**(예: `reservation.created` / `reservation.failed`)
으로 전달되며, 동일 `outbox_id` 로 요청과 상관관계를 맺는다. 완료 이벤트의 리소스
envelope 는 동기 성공 응답과 동일한 형태다.

## 4. 예약 (Reservations)

### POST /api/v1/reservations

`reservations:write` · `Idempotency-Key` 필수.

| 필드                             |  필수 | 설명                                                                         |
| ------------------------------ | :-: | -------------------------------------------------------------------------- |
| `room_type_id`                 |  ✓  | VPMS 객실 타입 id                                                              |
| `type`                         |  ✓  | `lodge`(숙박) 또는 `rent`(대실)                                                  |
| `start_date`                   |  ✓  | 체크인 `YYYY-MM-DD`                                                           |
| `end_date`                     |  ✓  | 체크아웃 `YYYY-MM-DD` (rent 는 = start\_date)                                   |
| `check_in_at` / `check_out_at` |     | 실제 입실/퇴실 **시각** (ISO datetime). 미지정 시 날짜 기준 생성 — 시각이 있는 원천 데이터는 반드시 함께 보내라 |
| `nightly_rates`                |     | 박별 가격 배열. 길이 = 박수(rent 는 1). 지정 시 요금제 자동가격을 **우회**해 원 단위 패리티               |
| `rate_plan_id`                 |     | 미지정 시 VPMS 가 자동 해석(최저가 플랜)                                                 |
| `channel_id`                   |     | 판매 채널(거래처) id. 채널에 거래처가 연결돼 있으면 대외후불 연동                                    |
| `status`                       |     | `INQUIRY`/`TENTATIVE`/`DEFINITE`… (기본 `DEFINITE`)                          |
| `sleeps` / `child_sleeps`      |     | 인원                                                                         |
| `ota_code`                     |     | 외부 예약번호                                                                    |
| `memo`                         |     | 메모                                                                         |
| `guest`                        |     | `{ name, phone, email, country, vehicle_number }`                          |

```json theme={null}
{
  "room_type_id": "01K...",
  "type": "lodge",
  "start_date": "2026-08-02",
  "end_date": "2026-08-04",
  "nightly_rates": [75000, 75000],
  "channel_id": 12,
  "status": "DEFINITE",
  "ota_code": "123944",
  "guest": { "name": "홍길동", "phone": "010-0000-0000" }
}
```

`nightly_rates` 지정 시 합계가 예약 총액이 되고, 요금제는 분류(리포팅) 용도로만 쓰인다.

### 그 밖의 예약 엔드포인트

| 메서드      | 경로                               | scope                | 비고                                                                             |
| -------- | -------------------------------- | -------------------- | ------------------------------------------------------------------------------ |
| `GET`    | `/api/v1/reservations/:id`       | `reservations:read`  | 동기 조회                                                                          |
| `PATCH`  | `/api/v1/reservations/:id`       | `reservations:write` | 기간 변경 시 요금제 재해석                                                                |
| `DELETE` | `/api/v1/reservations/:id`       | `reservations:write` | 취소 (`cancel_payment` 옵션)                                                       |
| `POST`   | `/api/v1/reservations/:id/purge` | `reservations:write` | **물리 삭제** — 예약+folio+청구+결제 행 제거 (취소·soft-delete 아님). 예약 1건짜리 단독 folio 만 허용. 멱등 |
| `GET`    | `/api/v1/commands/:key`          | `reservations:read`  | 이 설치가 특정 Idempotency-Key 로 만든 명령의 저장 결과 조회 (부수효과 없음)                           |

## 5. 결제 (Payments)

### POST /api/v1/payments

`folios:write` · `Idempotency-Key` 필수. 결제는 folio 에 귀속되므로 `folio_id` 또는
`reservation_id`(→folio 해석) 중 하나가 필요하다.

| 필드                               |  필수 | 설명                                                              |
| -------------------------------- | :-: | --------------------------------------------------------------- |
| `reservation_id` 또는 `folio_id`   |  ✓  | 귀속 folio                                                        |
| `amount`                         |  ✓  | 금액                                                              |
| `method`                         |  ✓  | `account` `cash` `creditCard` `easy` `etc` `mileage` `platform` |
| `status`                         |     | 결제 상태 (예: `APPROVED`)                                           |
| `transaction_id` / `approval_id` |     | 거래·승인 번호                                                        |
| `tax_type`                       |     | 과세 유형                                                           |
| `memo`                           |     | 메모                                                              |

<Note>
  `method: "platform"` 은 VPMS 의 **대외후불**이다 — 예약에 연결된 채널(거래처)의 미수로
  처리된다. 따라서 platform 결제는 예약이 `channel_id`(거래처 연결 채널)를 가질 때
  의미가 있다.
</Note>

| 메서드      | 경로                     | scope          |
| -------- | ---------------------- | -------------- |
| `PATCH`  | `/api/v1/payments/:id` | `folios:write` |
| `DELETE` | `/api/v1/payments/:id` | `folios:write` |

## 6. 조회 (Reads)

| 메서드   | 경로                       | scope            | 설명       |
| ----- | ------------------------ | ---------------- | -------- |
| `GET` | `/api/v1/accommodation`  | (bearer)         | 설치 숙소 정보 |
| `GET` | `/api/v1/room-types`     | `rooms:read`     | 객실 타입 목록 |
| `GET` | `/api/v1/room-types/:id` | `rooms:read`     | 객실 타입 상세 |
| `GET` | `/api/v1/rates`          | `rates:read`     | 일별 요금    |
| `GET` | `/api/v1/availability`   | `inventory:read` | 재고/가용성   |

## 7. UI Surface 데이터 (매핑)

설정 화면(`manifest.surfaces`) 을 뒷받침하는 dataset/매핑 API. 자세한 블록 선언은
[UI Kit Manifest 가이드](/guides/marketplace-ui-kit-manifest) 참조.

| 메서드    | 경로                                      | scope                        | 설명                             |
| ------ | --------------------------------------- | ---------------------------- | ------------------------------ |
| `PUT`  | `/api/v1/datasets/{key}`                | `marketplace:datasets:write` | 매핑 후보 항목 푸시(upsert)            |
| `GET`  | `/api/v1/datasets/{key}`                | `marketplace:datasets:write` | 푸시한 dataset 조회                 |
| `GET`  | `/api/v1/mappings`                      | `marketplace:mappings:read`  | 호텔이 확정한 매핑 조회                  |
| `POST` | `/api/v1/installation/setup-ready`      | `marketplace:datasets:write` | explicit 준비 신호                 |
| `POST` | `/api/v1/action-runs/{run_id}/complete` | (bearer)                     | action(completion=event) 완료 보고 |

### PUT /api/v1/datasets/{key}

`key` 는 `^[a-z0-9][a-z0-9_-]{0,63}$`. 항목은 최대 5,000개.

```json theme={null}
{ "items": [ { "id": "AGODA", "label": "아고다", "meta": {} } ] }
```

### GET /api/v1/mappings

`?vpms_entity=<entity>&dataset=<key>` 로 필터.

```json theme={null}
{
  "mappings": [
    { "vpms_entity": "roomType", "vpms_entity_id": "01K...",
      "dataset": "ext_room_types", "external_id": "RT-1",
      "external_label": "스탠다드", "updated_at": "…" }
  ]
}
```

### POST /api/v1/action-runs/{run_id}/complete

`{ "status": "completed" | "failed", "message": "27건 동기화" }` — 이미 종결된 run 은
현재 상태를 멱등 재응답한다.

## 8. 사용량 / 컨텍스트

| 메서드    | 경로                     | scope                      | 설명                                                |
| ------ | ---------------------- | -------------------------- | ------------------------------------------------- |
| `GET`  | `/api/v1/me`           | (bearer)                   | 호출 주체(설치·client·scope)                            |
| `GET`  | `/api/v1/installation` | (bearer)                   | 설치 상세 — `setup_status`, `config`(configForm 값) 포함 |
| `GET`  | `/api/v1/usage`        | `marketplace:usage:read`   | endpoint family 별 호출량                             |
| `POST` | `/api/v1/events`       | `marketplace:events:write` | Robust 이벤트 적재(202, outbox)                        |

## 9. 에러 형식

```json theme={null}
{ "error": "invalid_request", "error_description": "room_type_id is required" }
```

| HTTP | error                              | 의미                 |
| :--: | ---------------------------------- | ------------------ |
|  400 | `invalid_request`                  | 검증 실패 / 필수 헤더 누락   |
|  401 | `invalid_client` / `invalid_token` | 인증 실패              |
|  403 | `forbidden`                        | scope 부족           |
|  404 | `not_found`                        | 리소스 없음             |
|  502 | `upstream_error`                   | 내부 백엔드 오류 (재시도 가능) |
