주문 연동하기
주문을 수집하고 발주확인·송장 등록·배송완료까지 처리하는 방법이에요.
주문 상품(라인) 상태 흐름
상태는 주문 상품(라인) 단위로 관리돼요. 한 주문에 여러 상품이 있으면 상품마다 다른 상태를 가질 수 있어요. 응답 필드는 item_status예요.
| 상태 | 설명 | 오픈 API 노출 |
|---|---|---|
pending | 결제 진행 중 | 노출 안 됨 |
paid | 결제완료 — 발주확인 대기 | 노출 |
preparing | 상품준비중 | 노출 |
shipping | 배송중 — 송장 등록됨 | 노출 |
delivered | 배송완료 | 노출 |
confirmed | 구매확정 — 종결 | 노출 |
canceled | 취소 완료 — 종결 | 노출 |
returned | 반품 완료 — 종결 | 노출 |
exchanged | 교환 완료 — 종결 | 노출 |
failed | 결제 실패 — 종결 | 노출 안 됨 |
expired | 결제 만료 — 종결 | 노출 안 됨 |
pending·failed·expired는 결제가 완료되지 않은 라인의 상태예요. 오픈 API는 결제 완료 주문만 다루므로 이 세 값은 응답에 나타나지 않아요.- 운영 보정은 뷰리티 운영자가 잘못된 처리를 되돌리는 경로예요. API로는 호출할 수 없지만, 증분 수집 중에 상태가 뒤로 가는 것을 관측할 수 있어요. 상태가 단조 증가한다고 가정하지 마세요.
- 종결 상태(
confirmed·canceled·returned·exchanged)에서 나가는 전이는 없어요.
paid → preparing)을 거치는 흐름을 권장하지만, paid 상태에서 바로 송장을 등록해도 돼요(그 경우 발주확인 단계는 건너뛴 것으로 처리돼요). 창고에서 곧바로 출고하는 운영을 위한 경로예요. 다만 라인은 preparing을 거치지 않고 shipping으로 바로 넘어가므로, preparing으로의 전환 이벤트를 기다리는 로직을 두지 마세요.order_status는 별개 값이에요. 주문에 포함된 모든 셀러의 상품 상태에서 계산되고 paid·preparing·shipping·delivered·confirmed·partially_claimed·canceled 중 하나예요. 다른 셀러 상품 때문에 값이 흔들리므로 처리 판단에는 쓰지 마세요.증분 수집
주문·클레임·문의 목록은 "이 시각 이후에 변경된 것"만 가져오는 방식으로 수집해요.
| 항목 | 권장값 |
|---|---|
| 수집 주기 | 5분 |
조회 구간 시작(updated_at_gte) | 마지막 수집 시각보다 10분 앞선 시각 |
| 저장 방식 | 식별자 기준 덮어쓰기(upsert) |
구간을 겹쳐 조회하는 건 경계에서의 누락을 막기 위해서예요. 같은 건이 두 번 와도 upsert로 저장하면 문제되지 않아요. 중복 없이 정확히 한 번만 받는 것(exactly-once)은 보장하지 않아요.
대량 일괄 변경으로 여러 건이 같은 updated_at을 갖는 경우에도 커서 방식이라 누락·중복 없이 페이지를 넘겨요.
주의할 점
- 다음 페이지를 요청할 때 필터 조건을 그대로 유지해야 해요.
cursor만 바꿔요. - 한 회차가 끝나면 커서를 버리고, 다음 회차는 새
updated_at_gte로 커서 없이 다시 시작해요. - 클레임(
/open/v1/claims)·문의(/open/v1/qna)도 같은 방식이에요. 세 목록을 각각 수집해요. - 페이지마다 행 수 한도(분당 3,000행)가 누적돼요. 초기 전체 적재 때는 429를 만날 수 있으니
Retry-After를 지켜 주세요.
조회 축 두 가지
목적이 달라요. 섞어 쓰면 원하는 결과가 나오지 않아요.
| 축 | 파라미터 | 의미 | 용도 |
|---|---|---|---|
| 변경 시각 | updated_at_gte / updated_at_lte | 그 건의 정보가 마지막으로 바뀐 시각 | 정기 증분 수집. 무엇이 바뀌었는지 빠짐없이 따라가기 |
| 상태 전환 시각 | status_changed_to + status_changed_at_gte / status_changed_at_lte | 지정한 상태로 전환된 이벤트가 그 구간에 있었는지 | 업무 단위 집계. "어제 발송된 주문", "지난주 취소된 주문" |
| 결제 시각 | paid_at_gte / paid_at_lte | 결제 완료 시각 | 주문 유입 기준 조회 |
상태 전환 시각 조회 규칙
status_changed_to와 시각 필터는 함께 보내야 해요. 한쪽만 보내면 422 STATUS_CHANGED_FILTER_INCOMPLETE예요. 허용 조합은 넷이에요.
| 조합 | 결과 |
|---|---|
| 셋 다 미지정 | 통과(상태 전환 필터 미적용) |
to + gte | 통과 |
to + lte | 통과 |
to + gte + lte | 통과 |
그 밖(시각만 지정·to만 지정) | 422 |
gte가lte보다 뒤면 422STATUS_CHANGED_RANGE_INVALID예요.- 지정할 수 있는 상태는 8종이에요:
paid·preparing·shipping·delivered·confirmed·canceled·returned·exchanged - 의미는 "그 상태로 전환된 이벤트가 구간 안에 존재하는 주문"이에요. 같은 상태에 두 번 진입해도 주문은 한 번만 반환돼요.
- 취소·환불 신청 시각 기준으로 찾고 싶다면 이 축이 아니라 클레임 API의
requested_at_gte/requested_at_lte를 써요. 주문 축의canceled는 "취소가 완료되어 주문 상품이 취소 상태가 된" 시각이에요.
발주확인부터 배송완료까지
| 단계 | 엔드포인트 | 요청 본문 | 주의점 |
|---|---|---|---|
| 발주확인 | POST /open/v1/orders/{order_no}/preparation | item_ids[] | 비어 있으면 422. paid 라인만 대상 |
| 송장 등록 | POST /open/v1/orders/{order_no}/shipment | item_ids[], carrier_name, tracking_no, logistics_company(선택) | 택배사는 허용 5종 표기 그대로. paid·preparing 라인 대상(paid면 발주확인을 건너뛴 것으로 처리) |
| 일괄 송장 | POST /open/v1/orders/batch/shipment | items[](주문별 entry, 최대 500) | 부분 성공. HTTP는 항상 200. 대상 상태는 단건과 같아요 |
| 배송완료 | POST /open/v1/orders/{order_no}/delivery | item_ids[] | shipping 라인만 대상 |
공통 사항
- 모든 처리 요청에
Idempotency-Key헤더가 필수예요. item_ids에는 주문 조회 응답의items[].item_id(UUID)를 넣어요.item_code가 아니에요.- 없는 ID·다른 주문의 ID·다른 셀러의 ID·형식이 틀린 값은 모두 같은 404
ORDER_ITEM_NOT_FOUND예요. - 이미 목표 상태인 라인은 오류가 아니라 성공이고 응답의
already_done이true예요. - 취소·반품·교환이 진행 중인 라인은 409
ORDER_ITEM_HAS_ACTIVE_CLAIM으로 거절돼요. 클레임을 먼저 처리해 주세요.
택배사 표기
아래 5종의 표기를 그대로 보내요. 목록 밖 값은 422 SHIPMENT_CARRIER_NOT_ALLOWED이고, 이 오류는 아무것도 바꾸지 않아요.
CJ대한통운 · 우체국택배 · 한진택배 · 롯데택배 · 로젠택배
일괄 송장 등록의 응답 판정
{
"meta": { "code": 200, "message": "OK", "error_code": null, "error_data": null },
"data": {
"succeeded": ["0f8b7c21-4d3e-4a55-9b16-2c7d8e9f0a1b"],
"failed": [
{
"order_no": "VR20260826-0000009",
"order_item_public_ids": ["5a1c2d33-8e47-4b90-b6f2-1d0e9a7c4b58"],
"error_code": "ORDER_ITEM_NOT_SHIPPABLE",
"message": "발송 처리할 수 없는 상태의 상품입니다."
}
],
"summary": { "succeeded": 1, "failed": 1 }
}
}
succeeded는 처리에 성공한 주문 상품 고유 ID(UUID) 목록이고,summary의 두 값도 주문 수가 아니라 주문 상품 수예요.failed[].order_item_public_ids는 주문 자체를 찾지 못한 경우 빈 배열이에요.- 전건 성공·부분 성공·전건 실패가 모두 HTTP 200이에요. HTTP 상태가 아니라
data.failed[]로 판정해 주세요. - entry마다 별도 트랜잭션이라 한 주문의 실패가 다른 주문에 영향을 주지 않아요.
상태별로 채워지는 데이터
| 필드 | paid | preparing | shipping | delivered | confirmed |
|---|---|---|---|---|---|
carrier_name | null | null | 값 | 값 | 값 |
tracking_no | null | null | 값 | 값 | 값 |
logistics_company | null | null | 값 또는 null | 좌동 | 좌동 |
shipped_at | null | null | 값 | 값 | 값 |
delivered_at | null | null | null | 값 | 값 |
confirmed_at | null | null | null | null | 값 |
has_active_claim | 클레임 진행 중이면 true | 좌동 | 좌동 | 좌동 | false |
is_exchange는 교환으로 새로 만들어진 상품이면 true예요. 상태와 무관하게 고정된 값이에요.
주문 헤더의 order_no·paid_at·currency·금액 필드는 조회되는 모든 주문에 항상 담기고, 주문자·수취인 정보(orderer_name·recipient_name·주소·ship_memo 등)도 항상 담기지만 회원 탈퇴 후에는 null이에요. updated_at은 주문·라인·송장·클레임이 바뀔 때마다 갱신돼요.
엣지 케이스
부분 클레임으로 주문 상품이 나뉘는 경우
고객이 3개 중 1개만 취소하면 뷰리티는 그 주문 상품을 둘로 나눠요(취소 1개 / 잔여 2개). 새로 만들어진 쪽은 새 item_id(UUID)를 갖고, 주문 조회 응답에서는 새 item_code(다음 순번)도 함께 받아요.
| 식별자 | 성질 | 용도 |
|---|---|---|
item_id (UUID) | 줄마다 하나씩, 그 줄에 고정 | 연동사 시스템의 저장·갱신(upsert) 키. 처리 API의 item_ids에 넣는 값 |
item_code ({order_no}-{NN}) | 어느 응답에 담겼느냐에 따라 가리키는 줄이 달라져요 — 주문 응답에서는 분할된 줄이 새 라인 번호를 부여받아 다음 순번의 새 item_code를 갖고, 원본 라인 번호를 그대로 유지하는 건 클레임 응답의 item_code뿐이에요 | 원본 주문 라인 단위로 묶어 대사할 때 쓰는 기준. 다만 다리로 삼을 값은 클레임 응답의 item_code예요 |
같은 라인 -01(수량 3) 중 1개가 취소로 완결되면 응답이 이렇게 갈려요.
| 응답 | item_id | item_code | quantity | 비고 |
|---|---|---|---|---|
| 주문 조회 | 처음 받은 값 그대로 | …-01 | 2 | 남은 수량 줄(원래 줄) |
| 새 값 | …-02 | 1 | 분리된 canceled 줄 — 코드도 새로 붙어요 | |
| 클레임 조회 | 분리된 줄의 값 | …-01 | 1 | item_code는 원본 라인을 가리켜요 |
item_code를 받으므로, 주문 응답 안에서 같은 코드끼리 더해도 원래 수량이 복원되지 않아요. 원본 라인 기준 합산은 클레임 응답의 item_code를 다리로 삼아요 — 위 예에서 -01의 남은 수량 2 + item_code가 -01인 클레임의 신청 수량 1 = 원래 주문 수량 3이에요.- 행 단위 저장·중복 판정은
item_id로 해요. 줄마다 고유하고 그 줄에 고정된 값이라 upsert 키로 안전해요. - 원본 라인 묶음·대사는
item_code로 하되, 기준은 클레임 응답의item_code예요. 클레임이 어느 주문 라인에서 갈라져 나왔는지는 이 값으로만 알 수 있어요(주문 응답의 코드는 분할된 줄에 새로 붙어요). - 처리 API 호출 직전에는 주문 상세를 다시 조회해 그 시점의
item_id를 써요. 오래된item_id를 들고 있으면 404ORDER_ITEM_NOT_FOUND가 나요. - 그래서 클레임 응답에서는
item_code와item_id가 서로 다른 줄을 가리킬 수 있어요. 코드는 원본 라인, ID는 지금 그 클레임에 묶여 있는 라인이에요.
이미 발송된 주문에 다시 송장을 등록하는 경우
"같은 송장인가"는 세 값이 모두 같은지로 판정해요 — carrier_name·tracking_no·logistics_company. 앞의 두 값은 서버가 정규화한 뒤 비교하고, logistics_company는 보낸 값 그대로 비교해요.
| 상황 | 결과 |
|---|---|
| 세 값이 모두 같음 | 200, already_done: true(재시도로 간주) |
| 택배사 또는 송장번호가 다름 | 409 SHIPMENT_STATE_CONFLICT |
택배사·송장번호는 같은데 logistics_company가 다름(한쪽이 null인 경우 포함) | 409 SHIPMENT_STATE_CONFLICT |
null" 차이로 취급돼 409가 나요. 처음 보낸 값을 그대로 다시 보내 주세요. 반대로 처음에 안 보냈다면 재전송 때도 보내지 않아야 해요.송장 정정은 API로 되지 않아요. 셀러가 파트너센터에서 처리해요. 409를 받으면 주문 조회로 등록된 송장 세 값을 확인해 연동사 시스템 쪽 값을 맞춰 주세요.
혼합 주문
한 주문에 다른 셀러의 상품이 섞여 있으면 items[]에는 요청한 키의 셀러 상품만 담겨요. 금액 필드(seller_로 시작)도 그 상품들만 합산해요. order_status만 다른 셀러 상품까지 반영해 계산되므로, 자기 상품이 전부 shipping인데 order_status가 preparing으로 보일 수 있어요. 정상이에요.
상태가 뒤로 가는 경우
뷰리티 운영자가 잘못된 처리를 되돌리면 shipping → preparing처럼 상태가 역행해요. 증분 수집에서 이를 관측할 수 있으니 상태 전이를 단조 증가로 가정한 로직(예: "이미 delivered면 무시")을 두지 마세요. 수신한 상태를 그대로 반영하는 upsert가 안전해요.