마켓플레이스 앱이 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 로
한정된다.
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...
응답:
{
"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 이다.
OAuth client(client_id/client_secret)는 앱 등록 후 콘솔에서 1회 발급된다. secret 은
그 시점에만 노출되므로 안전하게 보관하라. installation_id 는 테넌트가 앱을 설치·동의한
뒤 installation.activated webhook 으로 전달된다.
2. 멱등성 (쓰기 필수)
모든 쓰기(POST/PATCH/DELETE)는 Idempotency-Key 헤더가 필수다. 없으면
400 invalid_request. 같은 키의 재요청은 최초 명령의 결과를 그대로 재응답하므로,
채널 매니저의 네트워크 재시도가 중복 예약/결제를 만들지 않는다.
POST /api/v1/reservations
Authorization: Bearer <token>
Idempotency-Key: your-app:resv:12345
Content-Type: application/json
키는 의미 있는 자연 키를 쓰라 (예: <app>:resv:<외부예약번호>). 랜덤 UUID 를 매
재시도마다 새로 만들면 멱등성이 깨진다.
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 } |
{
"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 | | 메모 |
method: "platform" 은 VPMS 의 대외후불이다 — 예약에 연결된 채널(거래처)의 미수로
처리된다. 따라서 platform 결제는 예약이 channel_id(거래처 연결 채널)를 가질 때
의미가 있다.
| 메서드 | 경로 | 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 가이드 참조.
| 메서드 | 경로 | 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 는 ^[a-z0-9][a-z0-9_-]{0,63}$. 항목은 최대 5,000개.
{ "items": [ { "id": "AGODA", "label": "아고다", "meta": {} } ] }
GET /api/v1/mappings
?vpms_entity=<entity>&dataset=<key> 로 필터.
{
"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//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. 에러 형식
{ "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 | 내부 백엔드 오류 (재시도 가능) |