---
title: "Response Schema"
description: "SearchResult, Product, ReviewPage, CategoryList, ShippingQuote — 모든 마켓플레이스가 돌려주는 정규화된 타입을 필드 단위로."
---

다섯 가지 타입이 모든 마켓플레이스 오퍼레이션을 덮습니다. 한 필드는 어디에 나타나든 같은 것을 뜻하며,
모든 개념에는 이름이 정확히 하나입니다 — 배송은 검색 카드, 상품, 오퍼, 배송 견적 어디서나 같은 형태입니다.

선택 필드는 말 그대로 선택입니다. **없다는 것은 마켓플레이스가 말하지 않았다는 뜻**이지, 기본값이
아닙니다. `price`가 없는 것은 무료 상품이 아니라 현재 판매 오퍼가 없는 리스팅입니다. `delivery.cost`가
없는 것은 무료배송이 아니라 배송비가 아직 산정되지 않았다는 뜻입니다 — 무료배송은 `delivery.free: true`,
전혀 다른 주장입니다.

어떤 선택 필드가 실제로 채워지는지는 마켓플레이스마다 크게 다릅니다. 각 마켓플레이스 문서에 측정된
비율이 있습니다.

## SearchResult

```json
{
  "marketplace": "amazon",
  "query": "running shoes",
  "page": { "index": 1, "size": 48, "total": 1200 },
  "items": [ /* SearchItem */ ],
  "cursor": "…"
}
```

| 필드 | 타입 | 비고 |
|---|---|---|
| `marketplace` | string | 어느 마켓플레이스가 답했는지 |
| `query` | string? | 이미지 검색에는 없음 — 기준이 사진이었으므로 |
| `page.index` | number | 1부터 시작 |
| `page.size` | number | 요청한 크기가 아니라 실제로 돌아온 개수 |
| `page.total` | number? | 목록 전체의 항목 수. 마켓플레이스가 밝힌 정확한 값 그대로이며, 반올림된 수치("260,000+")라면 빠집니다. 전부 받을 수 있다는 약속은 아닙니다. 다음 페이지가 없으면 거기서 끝납니다 |
| `items` | SearchItem[] | |
| `cursor` | string? | 불투명 값. 커서로 페이지를 넘기는 마켓플레이스만 내보냄 |
| `facets` | Facet[]? | 이 질의에 대한 마켓플레이스 자체의 필터 레일 — 아래 참고 |

### Facet

이 질의에 대한 사이트 자체의 필터 레일입니다. 없으면 레일이 돌아오지 않은 것이고, `[]`는 이 질의에
레일이 없다는 뜻입니다.

| 필드 | 타입 | 비고 |
|---|---|---|
| `id` | string | 그룹의 키: `brand`, `sock_rise` |
| `name` | string | 표시 이름: "Brand", "Sock Rise" |
| `kind` | `select` \| `range` | `select`는 값을 나열하고, `range`는 범위를 밝힘 |
| `values[]` | FacetValue | `{value, name, count?, selected?}` |
| `min` `max` | number? | `range`용. 금액 범위는 `Money.amount`와 같은 단위 |
| `maps_to` | string? | 같은 개념을 다루는 타입 있는 필터, 예: `brands` |

`value`는 `facets` 요청 파라미터로 그대로 돌려보내는 값입니다. 내부 id가 아니라 사람이 읽는 표시
이름입니다.

`maps_to`가 있으면 그 타입 있는 파라미터를 쓰세요. 모든 마켓플레이스에서 같은 것을 뜻합니다.

## SearchItem과 Product

둘 다 하나의 아이템 형태를 확장하므로, 한 필드는 결과 카드에서나 상품 페이지에서나 같은 것을 뜻합니다.
`Product`는 상세 화면에만 있는 것을 더합니다.

### 둘이 공유하는 필드

| 필드 | 타입 | 비고 |
|---|---|---|
| `marketplace` `id` `title` | string | 항상 있음 |
| `url` | string? | 리스팅 자체의 페이지 |
| `price` | Money? | 구매자가 오늘 지불하는 금액 |
| `listPrice` | Money? | 할인 기준이 된 취소선 가격 |
| `condition` | string? | 표기된 그대로: `"Pre-Owned"`, `"Used: Very Good"`, `"New"` |
| `conditionNote` | string? | 그 상태에 대한 판매자 자신의 설명 |
| `unitPrice` | object? | `{price, unit}` — `"$1.00 / count"` |
| `brand` | string? | 사양 표에서 끌어올린 값. "Brand" 행은 `attributes`에 반복되지 않음 |
| `identifiers` | object? | `{gtin?, mpn?, model?}` — 바코드(UPC/EAN/ISBN), 부품/모델 번호, 모델명 |
| `rating` | Rating? | `{average, count?, distribution?, grouped?}` |
| `seller` | Seller? | `{id?, name?, url?, rating?, country?, detailedRatings?, positivePercent?}` — 아래 참고 |
| `salesCount` | number? | 정규화된 판매 수량. 마켓플레이스가 반올림하므로 **하한값** |
| `salesLabel` | string? | 문구 그대로: `"3,000+ sold"` |
| `inStock` | boolean? | |
| `stock` | number? | 판매 가능 수량. 마켓플레이스가 숫자로 밝히는 경우 |
| `status` | string? | 표기된 그대로의 재고 문구: `"Only 3 left in stock"` |
| `delivery` | DeliveryOption? | 기본 배송 방식 — 아래 참고 |
| `shippingOptions` | DeliveryOption[]? | 표시되거나 견적된 모든 옵션, 기본 옵션이 처음 |
| `returnsPolicy` | string? | |
| `badges` | string[] | 머천다이징 배지: `["Amazon's Choice"]` |
| `promotions` | string[] | 할인: `["5% off coupon applied"]` |
| `paymentOptions` | string[]? | 결제 방법: `["Pay in 4 payments at 0% interest"]` |
| `listingFormat` | string? | 판매 방식: `"Buy It Now"`, `"Auction"` |
| `groupId` | string? | 이 상품이 속한 변형 패밀리 |
| `attributes` | Attribute[] | `{name, value}` 쌍 — 사양 표에서 타입 있는 필드로 끌어올린 행을 뺀 나머지 |
| `offersSummary` | object? | `{count?, fromPrice?, url?}` — 목록이 아니라 요약 |
| `priceRange` | object? | Money로 된 `{min, max}` — 마켓플레이스가 밝힌 변형 간 가격 폭("$12.99 – $24.99"). 일부 변형만 보고 계산하지 않습니다 |
| `endsAt` | string? | 현재 조건의 판매가 끝나는 시각. 경매 마감이나 기간 한정 딜의 종료. ISO 8601 |
| `purchaseLimit` | number? | 구매자 한 명이 이 상품을 주문할 수 있는 최대 수량("1인 2개 한정") |
| `listedAt` | string? | 리스팅이 마켓플레이스에 처음 게시된 날짜(수집 시각이 아님). ISO 8601 날짜 또는 시각 |
| `deliveryTo` | object? | `{label?, country?, postalCode?}` — `delivery`와 `shippingOptions`의 배송 약속이 기준으로 삼은 배송지 |
| `ext` | object? | 공통 필드에 담을 곳이 없는, 그 마켓플레이스에만 있는 사실 — 아래 참고 |

`Money`는 `{currency, amount}`이며 `amount`는 십진 주단위입니다.

`ext`는 최후의 수단입니다. 같은 뜻의 공통 필드가 있으면 언제나 그 필드에 담고, 다른 마켓플레이스에는 없는
사실만 그 마켓플레이스의 이름 그대로 `ext`에 담습니다. 상품, 변형, 판매자, 오퍼, 리뷰, 리뷰 페이지,
카테고리 노드에 붙을 수 있습니다.

### DeliveryOption

모든 배송이 쓰는 하나의 형태입니다 — `delivery`, `shippingOptions`의 각 항목, 오퍼의 `delivery`,
[ShippingQuote](#shippingquote)의 각 옵션.

| 필드 | 타입 | 비고 |
|---|---|---|
| `estimate` | string? | 표기된 그대로의 도착 예상: `"FREE delivery Wednesday, October 28"` |
| `cost` | Money? | 배송비 |
| `free` | boolean? | 마켓플레이스가 무료배송이라고 밝힘 |
| `minDays` `maxDays` | number? | 운송 기간(일), 숫자로 밝히는 경우 |
| `handlingDays` | number? | 판매자가 발송하기까지의 일수 |
| `guaranteedDays` | number? | 마켓플레이스 자체의 배송 보장 일수 |
| `cutoff` | string? | 이 옵션의 주문 마감 카운트다운: `"Order within 3 hrs 22 mins"` |
| `condition` | string? | 적용 조건: `"on orders shipped by Amazon over $35"` |
| `available` | boolean? | `false` — 요청한 지역으로는 배송하지 않음 |
| `membership` | string? | 이 옵션에 필요한 멤버십(예: `"Prime"`). 없으면 모든 구매자에게 해당 |
| `minimumOrder` | Money? | `cost`/`free`가 적용되는 최소 주문 금액 — Amazon의 "on $35 of items" |
| `carrier` `code` | string? | 마켓플레이스가 부르는 배송 서비스 |
| `tracking` | boolean? | |
| `shipsFrom` | string? | 출고 국가 |

`cost`가 없는 것은 무료가 아니라 **밝혀지지 않은 것**입니다. 무료는 `free: true`입니다. `available: false`는
답이며, 결코 0원 배송비가 아닙니다. 마켓플레이스가 보여 준다면 `delivery`는 언제나 **모든** 구매자에게 해당하는
옵션입니다 — 멤버 전용 무료 배송(Prime)은 `membership`과 함께 `shippingOptions`에 있고, 모두의 조건으로 표시되지 않습니다.

### Seller

| 필드 | 타입 | 비고 |
|---|---|---|
| `id` `name` `url` | string? | |
| `rating` | Rating? | 판매자 피드백 점수. 상품의 `rating`과 같은 형태 |
| `country` | string? | 판매자 소재 국가 |
| `detailedRatings` | object? | `{itemAsDescribed?, communication?, shippingSpeed?}`, 마켓플레이스의 0–5 척도 |
| `positivePercent` | number? | 판매자 피드백 중 긍정 평가의 비율, 0–100 퍼센트("99.8% positive"). 별점이 아닙니다 |

`rating.grouped: true`는 이 집계가 요청한 하나의 변형이 아니라 변형 패밀리 전체를 다룬다는 뜻입니다.
이 값이 없다는 것은 *묶여 있는지 알 수 없다*는 뜻이며, "변형별임이 확인되었다"보다 약한 진술입니다.

### SearchItem에만 있는 필드

| 필드 | 타입 | 비고 |
|---|---|---|
| `imageUrl` | string? | 카드의 대표 이미지 |
| `sponsored` | boolean? | 자연 검색 결과가 아니라 유료 노출 |
| `variantCount` | number? | 카드에 적힌 조합 수("+12 colors"). `product`는 이를 `variants`로 나열합니다 |
| `category` | CategoryStep[]? | 분류 체계에서 이 결과의 위치, 말단이 마지막. 카드에는 보통 말단만 있습니다 |
| `matchScore` | number? | 이미지 검색 전용. 보낸 사진과 얼마나 닮았는지를 0–1로 나타냄(1 = 마켓플레이스의 최상위 일치). 같은 결과 안에서만 비교할 수 있습니다 |

### Product에만 있는 필드

| 필드 | 타입 | 비고 |
|---|---|---|
| `description` | string? | HTML일 수 있음 |
| `features` | string[] | 페이지 순서대로의 불릿 셀링 포인트 |
| `media` | object | `{images: string[], videos: []}` |
| `category` | CategoryStep[] | 전체 경로, 말단이 마지막. 각 단계는 `id`, `name`, 또는 둘 다를 가짐 |
| `options` | Option[] | 축: 색상, 사이즈 |
| `variants` | Variant[] | 조합들 — 아래 참고 |
| `offers` | Offer[] | 마켓플레이스가 열거하는 경우의 경쟁 판매자 |
| `subscriptionPrice` | Money? | Subscribe & Save 등 |
| `reviewSummary` | string? | 마켓플레이스가 생성한 요약 |
| `reviewAspects` | array | 항목별 감성: `{name, mentions?, positive?, negative?}` |
| `boxContents` | string[] | 구성품 |
| `brandUrl` `fulfilledBy` `replacedBy` | string? | |
| `package` | object? | 발송 포장: `{lengthCm?, widthCm?, heightCm?, weightKg?, soldInLot?}` |
| `wholesale` | boolean? | 대량 구매가를 제공하는 리스팅 (`variants[].bulkPrice` 참고) |
| `reviews` | Review[] | 상품 페이지 자체가 싣고 있는 상위 리뷰 |
| `included` | string[]? | 요청한 확장 중 실제로 가져온 것 |

### 변형, 그리고 반드시 확인할 한 가지

세상에는 두 가지 변형 모델이 있고, 그 둘을 가르는 것은 한 필드입니다.

- **`variant.productId`가 있음** — 그 조합 자체가 조회 가능한 상품입니다. 그 id를 `product`에
  그대로 넘기세요. Amazon이 이 방식입니다.
- **`variant.productId`가 없음** — 그 조합은 이 상품 안에서만 존재하며, 필요한 것은 이미 다 받았습니다.
  Shopify·Walmart·AliExpress가 이 방식입니다.

어느 마켓플레이스와 이야기하는지 알 필요는 없습니다. 필드를 확인하세요.

| 변형 필드 | 타입 | 비고 |
|---|---|---|
| `id` | string | 마켓플레이스가 그 조합에 붙인 id |
| `productId` | string? | 있으면 ⇒ `product`로 조회 가능 (위 참고) |
| `sku` | string? | 판매자 자신의 재고 관리 코드 |
| `price` `listPrice` | Money? | |
| `bulkPrice` | Money? | 대량 구매 시 단가. `price`와 다를 때만 |
| `purchaseLimit` | number? | 구매자 한 명이 주문할 수 있는 최대 수량 |
| `taxIncluded` | boolean? | `price`에 세금이 포함되었는지 |
| `inStock` | boolean? | |
| `stock` | number? | 이 변형의 판매 가능 수량 |
| `identifiers` | object? | `{gtin?, mpn?, model?}` — 변형별로 공개된 경우, 이 조합의 바코드와 부품 번호 |
| `weightKg` | number? | 이 조합의 무게(킬로그램) |
| `selections` | array | `{optionId, valueId, optionName, valueName}` — 어떤 옵션 값의 조합인지 |

### Offer

마켓플레이스가 열거하는 경우의 경쟁 판매자: `{seller?, price, listPrice?, condition?, conditionNote?,
fulfilledBy?, inStock?, status?, delivery?}`. 더 싸지만 더 늦게 배송되는 오퍼는 다른 오퍼이므로
`delivery`는 DeliveryOption입니다.

## ReviewPage

```json
{
  "marketplace": "walmart",
  "productId": "996425081",
  "summary": { "average": 4.6, "count": 1204, "distribution": { "5": 78 } },
  "page": { "index": 1, "size": 10, "total": 990 },
  "reviews": [ /* Review */ ],
  "grouped": true
}
```

| 필드 | 타입 | 비고 |
|---|---|---|
| `summary` | Rating? | `reviews`를 뽑아낸 집합이 무엇이든 그것을 설명함 |
| `page.total` | number? | 마켓플레이스가 밝힌, 목록에 있는 리뷰 수. 모든 평점을 세는 `summary.count`보다 적을 수 있음(Walmart는 글이 있는 리뷰만 나열). **`0`은 리뷰가 없다는 뜻** |
| `reviews[]` | Review | `{id?, author?, rating, title?, body?, date?, country?, verified?, helpfulCount?, variant?, images[], incentivized?, reply?}` |
| `grouped` | boolean? | 이 상품만이 아니라 패밀리 전체의 리뷰라는 뜻 |
| `cursor` | string? | |

### 리뷰가 없는 상품

명시적인 0과 함께 `200`으로 답하므로, "리뷰 0건"을 자신 있게 표시할 수 있습니다.

```json
{ "marketplace": "shopify", "productId": "shop.com/handle", "page": { "index": 1, "size": 0, "total": 0 }, "reviews": [] }
```

`page.total: 0`은 상품에 정말로 리뷰가 하나도 없을 때만 보냅니다. 상품에 리뷰가 *있는데* 가져올 수
없다는 증거가 있다면 — 개수나 평점이 보고되거나 상품 페이지에 리뷰가 보인다면 — 0이 아니라 에러가
돌아옵니다. 따라서 빈 `reviews` 배열은 두 가지 중 하나를 뜻하며, 어느 쪽인지는 `page.total`이
말해 줍니다.

| `page.total` | 의미 |
|---|---|
| `0` | 이 상품에는 리뷰가 없음 |
| 그 밖의 값 (숫자이거나 없음) | 리뷰의 끝을 지나 페이지를 넘긴 것 |

0으로 답할 때는 `summary`가 없습니다. 보고할 평균이 없기 때문입니다.

`grouped`는 부가 정보가 아니라 반드시 밝혀야 할 사실입니다. 판매자는 한 색상의 페이지에 패밀리 전체의
리뷰 9,000건으로 답할 수 있고, 리뷰 자체에는 그런 표시가 전혀 없습니다 — 묶인 페이지에서 "이 상품의
평점"을 계산하면 패밀리의 평점이 나옵니다.

`review.incentivized: true`는 마켓플레이스가 대가(무료 제품, 샘플, 보상)를 받고 쓴 리뷰로 표시한 경우입니다.
없으면 표시가 없다는 뜻일 뿐, "대가 없이 썼다"는 뜻은 아닙니다. `review.reply`는 판매자의 공개 답글이며
`{body, date?, author?}` 형태입니다.

`review.variant`는 마켓플레이스가 밝히는 경우 그 리뷰가 어느 형제 상품에 쓰였는지를 알려줍니다. 묶인
페이지 안에서 `variant`가 비어 있다면 *마켓플레이스가 말하지 않았다*는 뜻이지, "요청한 상품의 것"이라는
뜻은 결코 아닙니다.

`page.total`과 `summary.count`는 세는 대상이 다릅니다. 어떤 상품은 평점 7,654건을 표시하면서 리뷰 목록에는 글이
있는 리뷰 990건만 담고 있을 수 있습니다. count는 상품에 대해, total은 목록에 대해 참입니다. 어느 쪽도 모든
리뷰를 끝까지 받을 수 있다는 약속은 아닙니다. 마켓플레이스마다 페이지를 넘길 수 있는 깊이에 한도가 있고, 목록은
`cursor`가 끝나는 곳에서 끝납니다.

## CategoryList

`categories`가 돌려주는 것: 하나의 평평한 목록이며, 모든 노드가 자신의 부모를 밝힙니다. 중첩이 아니라
평평한 이유는 흔한 요청이 "분류 전체"이고, 중첩하면 전송량이 커지고 페이지로 나누기도 어려워지기
때문입니다. 트리를 다시 만드는 일은 `parentId` 기준 `groupBy` 한 번입니다.

```json
{
  "marketplace": "aliexpress",
  "path": [ { "id": "200000345", "name": "Women's Clothing" } ],
  "categories": [
    { "id": "349", "name": "Skirts", "parentId": "200000345" },
    { "id": "201531101", "name": "Matching Sets", "parentId": "200000345" }
  ]
}
```

| 필드 | 타입 | 비고 |
|---|---|---|
| `marketplace` | string | |
| `path` | CategoryStep[] | 요청한 카테고리의 조상들, 말단이 마지막. 루트에서는 비어 있음 |
| `categories` | CategoryNode[] | |

### CategoryNode

| 필드 | 타입 | 비고 |
|---|---|---|
| `id` | string | **되돌려 넘기는 id** — 내려갈 때는 `categories`에, `browsable`이 허용하면 `browse`에 |
| `name` | string | |
| `parentId` | string? | 루트에는 없음 |
| `hasChildren` | boolean? | 없다는 것은 "아니오"가 아니라 **알 수 없음** — 대부분의 마켓플레이스는 밝히지 않음 |
| `browsable` | boolean? | 아래 참고 |
| `productCount` | number? | 마켓플레이스가 밝히는 경우에만. 페이지에서 세지 않음 |

이 스키마의 대부분과 달리 `id`와 `name`은 **둘 다 필수**입니다. 카테고리 목록은 무언가를 하기 위해
존재합니다. id가 없는 항목은 쓸 수 없고, 이름이 없는 항목은 읽을 수 없습니다.

### `browsable` — 특히 주의해서 읽을 값

**없으면 가능하다는 뜻**이고, 그것이 일반적인 경우입니다. `false`는 실재하고 탐색할 수 있지만 **뒤에
상품 목록 페이지가 없는** 단계를 표시합니다.

여기 있는 모든 마켓플레이스에 그런 단계가 있으며, 예외적인 경우가 아닙니다.

| | 나열할 수 없는 것 |
|---|---|
| Amazon | 부문 (browse 노드가 아니라 스토어 슬러그이므로) |
| Walmart | 일부 부문 — Electronics는 상품을 나열하지만 Toys는 그리드가 없는 허브 |

그런 단계를 만나면 **`categories`로 한 단계 더 내려가세요**. 거기 있는 id들은 나열됩니다.

`categories`에 **`browsableOnly=true`** 를 넘기면 `browse`가 받아들이는 id만 돌아옵니다 — 비용과 400이
나는 조건은 [마켓플레이스 개요](/docs/api-reference/marketplaces)를 보세요.

### 이 플래그를 믿을 필요는 없습니다

`browsable`은 확인된 경우에만 채워지므로, 나열할 수 없는 단계가 이 필드 없이 — 즉 낙관적으로 — 돌아올
수 있습니다. Walmart에서는 피할 수 없습니다. Electronics(`3944`)와 Toys(`4171`)는 둘 다 자식이 있는
부문인데 하나는 상품을 나열하고 다른 하나는 나열하지 않으며, 분류 정보만으로는 구별할 방법이 없습니다.

그래서 **`browse`가 직접 말해 줍니다**. 상품 목록 페이지가 없는 카테고리를 요청하면 빈 목록이 아니라

```json
{ "success": false, "error": { "code": "CATEGORY_NOT_BROWSABLE", "message": "Walmart category 4171 is a navigation hub…" } }
```

가 돌아옵니다. 빈 목록은 품절된 카테고리도 돌려주는 값이어서, "자식으로 내려가라"와 "여기에는 팔 것이
없다"를 구별할 방법이 없기 때문입니다. 이 코드는 같은 id로 `categories`를 부르고 그 자식 중 하나를
나열하라는 지시로 받아들이세요. 요청의 실패가 아니라 400이며, 재시도해도 답은 달라지지 않습니다.

## ShippingQuote

[`shipping`](/docs/api-reference/aliexpress/shipping)이 돌려주는 것: 한 상품을 한 지역으로 보내는 모든
방법, 가장 저렴한 것부터.

```json
{
  "marketplace": "aliexpress",
  "productId": "1005012143419303",
  "skuId": "12000057632572864",
  "region": "US",
  "quantity": 1,
  "available": true,
  "options": [
    { "carrier": "YunExpress Standard Shipping", "cost": { "currency": "USD", "amount": 3.13 },
      "free": false, "minDays": 7, "maxDays": 15, "guaranteedDays": 60,
      "estimate": "Oct. 30 - Nov. 07", "tracking": true, "shipsFrom": "CN" }
  ]
}
```

`options`는 [DeliveryOption](#deliveryoption)입니다. 옵션 없이 `available: false`이면 마켓플레이스가 그
지역으로 배송하지 않는다는 뜻이며, 에러가 아니라 답입니다.

## 에러

```json
{ "success": false, "error": { "code": "INVALID_PARAMS", "message": "…" } }
```

| 상태 | 코드 | 의미 |
|---|---|---|
| 400 | `INVALID_PARAMS` | 파라미터가 없거나, 형식이 잘못되었거나, 이 마켓플레이스가 받지 않는 것 |
| 400 | `CATEGORY_NOT_BROWSABLE` | 카테고리는 있으나 상품 목록 페이지가 없음 — `categories`로 내려가 자식을 나열하세요 |
| 404 | `NOT_FOUND` | 그런 id가 없음 |
| 404 | `UNKNOWN_MARKETPLACE` / `UNKNOWN_OPERATION` | `GET /v1/marketplaces`를 확인하세요 |
| 429 | 레이트 리밋 | `Retry-After`를 포함 |
| 501 | `UNSUPPORTED` | 데이터는 있으나 이 API로는 제공되지 않음 — 요청의 문제가 아님 |
| 502 | 업스트림 | 마켓플레이스를 사용할 수 없었음. 재시도하세요 |

501이 404가 아닌 것은 의도적입니다. "이 리뷰는 제공되지 않습니다"가 "이 상품에는 리뷰가 없습니다"로
읽혀서는 안 됩니다.
