<!-- Generated by scripts/generate-customer-oms-docs.mjs from the LLM guide and canonical OpenAPI. Do not edit directly. -->

# FKTG 자사몰 연동: LLM 개발 가이드

문서 버전: 2026-11-05. FKTG **Rate API(견적)**, **Ship API(배송 관리)**, **Tracking API(배송 추적)** 연동 지침입니다. 요청·응답 계약은 첨부 OpenAPI를 따릅니다.

## 파일을 전달하는 방법

1. 이 지침과 OpenAPI 전체가 포함된 [LLM 전달용 단일 파일](https://www.fktg.co.kr/guides/customer-oms/customer-oms-llm-handoff.ko.md)을 LLM 또는 코딩 에이전트에 첨부합니다.
2. 아래 작업 지시에 개발 언어, 프레임워크, 주문 이벤트와 필요한 기능을 덧붙입니다.

[고객 개발자 페이지](https://www.fktg.co.kr/developers)에서 API별 기능을 확인하고, [기존 API 문서](https://www.fktg.co.kr/platform/api-documentation)에서 고객 가이드·OpenAPI·Postman을 각각 내려받을 수 있습니다. 단독 지침만 첨부한다면 [OpenAPI](https://www.fktg.co.kr/guides/customer-oms/customer-oms-shipment-openapi.yaml)도 함께 첨부하세요.

## API 종류와 권한

`POST /auth/token`에 `Content-Type: application/json`으로 Client ID/Secret을 보내고, 나머지 API에는 `Authorization: Bearer {access_token}`을 붙입니다. 토큰은 내부 구조를 해석하지 않으며 응답의 `scope`로 각 권한을 확인합니다. 요청에 scope를 적는 것만으로 권한이 추가되지 않습니다.

| API | 기능 | 필요한 scope |
|---|---|---|
| Rate API | `POST /rates/quotes`: 목적지·중량·규격에 맞는 견적 조회. 배송은 생성하지 않습니다. | `rates.read` |
| Ship API | 생성 `POST /shipments`, 수정 `PATCH /shipments/{shipmentId}`, 취소 `POST /shipments/{shipmentId}/cancel` | `shipments.write` |
| Ship API | 상세·외부 참조·FKTG AWB 조회, 운송장 번호 `GET /shipments/{shipmentId}/awb`, FKTG house label PDF `GET /shipments/{shipmentId}/label` | `shipments.read` |
| Tracking API | `GET /shipments/{shipmentId}/tracking`: 해당 회사 배송의 저장된 상태를 polling으로 조회 | `shipments.read` |

Ship API의 `fktg_awb`는 FKTG 운송장 번호, `carrier_awb`는 에이전트 운송장 번호(Agent AWB)입니다. `carrier_awb`는 발급된 번호가 저장되기 전까지 `null`이며 할당 후 다시 조회합니다. AWB 조회는 운송사 라벨을 발행하지 않습니다. house label은 `application/pdf`로 처리하며 운송사 라벨 발행이나 출고 완료를 뜻하지 않습니다.

`GET /wms/tracking`은 `wms.tracking.read`가 필요한 별도 WMS 계약이며 이 OpenAPI의 범위가 아닙니다. 외부 WMS callback을 고객용 배송 웹훅으로 사용하거나 계약에 없는 재고·피킹·웹훅을 가정하지 않습니다. 필요한 연동은 담당자에게 별도 계약과 권한을 확인합니다.

## 그대로 전달할 작업 지시

```text
첨부한 전체 지침과 OpenAPI를 읽고 요청한 기능만 구현해 주세요.
자사몰의 언어·프레임워크·주문 이벤트·DB·큐·비밀 관리·테스트를 먼저 확인해 주세요.
기존 어댑터를 재사용하고 문서의 API 계약과 업무 규칙을 적용해 주세요.
환경과 보안 절차에 따라 mock부터 검증하고 승인된 범위에서 실제 연동으로 전환해 주세요.
식별자·재시도·통관·견적 규칙을 UI와 저장 로직에도 적용해 주세요.
테스트 체크리스트를 실행하고 mock·격리 DB·HTTP E2E·실 운송사 결과를 구분해 주세요.
실행한 명령과 결과, 남은 제약, 배포 전 상태와 rollback 방법을 보고해 주세요.
```

## 환경과 보안

운영 호스트는 `https://www.fktg.co.kr`, API base URL은 `https://www.fktg.co.kr/api/open/v1`입니다. OpenAPI의 `servers.url`은 호스트만, `paths`는 `/api/open/v1` 접두사를 포함하므로 접두사를 중복하지 않습니다. 공개 샌드박스는 없습니다. mock 검증 후 담당자가 회사 승인, credential 활성 상태, 부여된 scope, 사용 서비스와 호출 범위를 확인하면 승인된 운영 호출을 수행합니다. 공개 ID backfill과 새 앱 버전 전환도 끝나야 lifecycle을 활성화합니다. 배포 계약 확인 전에는 고객 실데이터·유료 운송사 요청·결제·출고를 실행하지 않습니다.

LLM에는 환경변수 이름과 합성 데이터만 전달합니다. 실제 Client ID/Secret과 토큰은 담당자가 백엔드 비밀 저장소에 설정하며 코드·프롬프트·예제 파일에 기록하지 않습니다. 운영 환경 파일과 고객 이름·주소·전화번호도 대화에 넣거나 요청하지 않습니다. 로그에는 `correlation_id`, HTTP status와 업무 code만 남기며 Secret/token/주소/전화/통관 ID는 기록하지 않습니다.

```dotenv
FKTG_MODE=mock
FKTG_BASE_URL=
FKTG_CLIENT_ID=
FKTG_CLIENT_SECRET=
```

위 변수는 고객사 어댑터의 예이며 FKTG 설정 API가 아닙니다. placeholder host를 실제 환경으로 사용하거나 임의 redirect로 다른 origin에 토큰을 전송하지 않습니다. 내부 DB·관리자 API·화면 scraping으로 빠진 계약을 우회하지 않습니다.

## 식별자와 재시도

생성 응답의 공개 `shipment_id`를 저장해 상세 조회와 후속 작업에 사용합니다. 기존 주문은 `external_shipment_ref` 또는 `fktg_awb` 조회로 공개 ID를 얻으며 내부 UUID를 요구하지 않습니다. 연동 테이블에는 고객 주문 식별자와 공개 `shipment_id`, 선택한 `service_code`, `fktg_awb`, `carrier_awb`를 보관합니다. AWB·우편번호·PID는 문자열로 처리해 선행 0을 보존합니다. 자사몰 주문 상태 변경과 FKTG 배송 생성은 별도 작업입니다.

| 상황 | 처리 |
|---|---|
| 토큰 만료 | `expires_in`으로 관리하고 동시 갱신을 합칩니다. |
| 401 | 1회 갱신 후에도 실패하면 오류를 노출합니다. |
| 403 | 회사 승인·credential 상태·scope를 확인하고 무한 재시도하지 않습니다. |
| 429 / 5xx | 각각 Retry-After / backoff·jitter를 적용합니다. 생성은 아래 참조 대사를 먼저 수행합니다. |
| 생성 timeout·409 또는 재시도 | 회사 전체에서 유일한 `external_shipment_ref`로 먼저 조회해 대사합니다. 새 참조로 재시도하거나 Idempotency-Key 지원을 가정하지 않습니다. |
| 취소 409 | 운영팀에 넘깁니다. 배송 취소는 결제 환불이나 운송사 void를 뜻하지 않습니다. |

공유 quota·freshness·SLA는 확인되지 않았습니다.

## 통관과 견적 규칙

- **EU PID:** EU27 + 현재 B2C 판정 + SALE에는 `export_reason`와 품목별 `merchantProductId`(M-PID), `nonStandardManufacturerProductId`(NS-PID)가 필요합니다. M·NS는 발송자가 정한 상품 식별자이며 같아도 됩니다. 아마존은 ASIN/FNSKU, 다른 플랫폼·자사몰은 상품관리코드(SKU)를 두 필드에 각각 입력합니다. `standardManufacturerProductId`(S-PID)는 제조자·브랜드가 국제규격으로 부여한 바코드(GTIN·EAN·UPC·ISBN 등)로 선택입니다. tax_id/IOSS를 PID로 매핑하거나 GB/NO/CH를 EU로 취급하지 않습니다. 회사명·GIFT·상품과 무관한 대체값으로 조건을 우회하지 않습니다.
- **품목 수정:** `items` PATCH는 전체 교체이므로 유지할 모든 품목과 PID를 보냅니다. SKU나 순서로 내부 품목을 추정하지 않으며 GET으로 PID 저장 결과를 확인합니다. 수취인·목적지·`export_reason` 변경도 재검증합니다. `origin_country`와 `origin_country_code`가 다르면 사용자가 수정해야 합니다.
- **견적과 경고:** 금액을 하드코딩하지 말고 견적 응답을 사용합니다. `requires_carrier_confirmation`과 생성 `warnings`를 UI에 반영하고, DAP→DDP 보정은 사용자에게 알린 뒤 동의 후 DDP를 명시합니다. PATCH는 부적합한 DAP를 422로 거절합니다.
- **Yun 상업 견적:** 2026-11-01 KST부터 신고 Item당 €3 관세 + €2 처리비를 1800원 환율로 환산한 9000원을 적용합니다. 이전 5400원에 더하지 않습니다. 동일 HS6+품명+원산지가 1 Item이며 quantity 합계가 아닙니다. 기존 €150/USD155 수락 제한이나 법정 과세 범위를 임의로 넓히지 않습니다. VAT 대납 수수료는 별도 후불 합의이며 FedEx/UPS에 Yun 선수취를 적용하지 않습니다. 실제 운송사 정산서 대조는 별도입니다.

## 포장 유형

기존 `declared_packaging_type`을 사용합니다. `BOX`는 종이 박스 등 고객 포장(MY PACKAGE), `CRATE`는 목재 상자, `PALLET`은 팔레트, `SKID`는 스키드입니다. `MY_PACKAGE`는 API 입력값이 아닙니다.

- Ship 생성의 `packages[]`와 Rate에서 생략한 유형은 `BOX`입니다. 기존 일반 박스 요청에는 새 필드를 추가하지 않아도 됩니다.
- Rate는 단일 포장물의 최상위 필드 또는 `packages[]`의 필드를 받습니다. 배열이 있으면 각 행의 값이 우선하며, 행에서 생략하면 최상위 값과 관계없이 `BOX`입니다.
- PATCH는 같은 `package_index`의 유형을 생략하면 저장값을 유지하고 `null`이면 지웁니다. 새 포장물의 생략값은 `BOX`입니다. `packages`를 보내면 전체 교체이므로 유지할 포장물도 포함합니다.
- 조회의 `declared_packaging_type`·`declared_pallet_piece_count`는 신고값이고 `packaging_type`·`pallet_piece_count`는 고객이 설정할 수 없는 창고 확정값입니다.
- `PALLET`·`SKID`와 UPS Worldwide Express Freight에는 각 단위의 `declared_pallet_piece_count`가 필요합니다. 적재된 개별 포장물 수를 1~99,999 정수로 보내며 합계도 99,999 이하여야 합니다. 팔레트 개수나 상품 수량을 복사하거나 1로 자동 입력하지 않습니다. PATCH 생략은 기존 수량을 유지하고, 수량이 필요하지 않은 포장·서비스에서 `null`로 지웁니다.
- UPS `BOX`·`CRATE`는 Freight 불가, `PALLET`·`SKID`는 일반 소포 불가이며 두 그룹을 한 발송물에 섞지 않습니다. 불일치는 검증 단계에 따라 422 `CARRIER_SERVICE_NOT_CUSTOMER_SELECTABLE` 또는 `INVALID_PACKAGING_SERVICE`입니다. 기존 Freight 요청은 실제 포장과 수량을 보완해야 할 수 있으며, 유형이 없는 기존 UPS Freight 발송물도 수량이 필요합니다. 수락 여부는 운송사 응답으로 확인합니다.

## 요청 예제

다음은 mock용 합성 요청입니다. 응답 schema와 오류 예제는 OpenAPI에 있습니다.

### Rate: `POST /rates/quotes`

```json
{
  "weight": 1.2,
  "item_count": 1,
  "incoterms": "DAP",
  "dimensions": { "length": 25, "width": 15, "height": 10 },
  "recipient": { "city": "New York", "state": "NY", "country": "US", "postal_code": "10001" }
}
```

Rate 응답의 `quotes`를 비교해 서비스 하나를 선택하고 예상 운임을 확인한 뒤, 해당 `service_code`로 Ship 생성을 한 번 호출합니다. 운임을 비교하려고 서비스별 발송물을 만들고 나머지를 취소하지 마세요. Rate 조회는 발송물·FKTG AWB·운송사 라벨을 생성하지 않으며 가격을 예약하지 않습니다. 신고 중량·규격에 따른 예상 운임이므로 창고 실측이나 추가 비용에 따라 최종 청구액이 달라질 수 있습니다.

`service_code`는 `FKTG-SVC-` 뒤에 대문자 16진수 32자리가 붙는 FKTG 고유 식별자입니다. 반환값을 그대로 저장하고 운송사 코드나 서비스명으로 직접 만들지 마세요. 생성에는 이 코드만으로 서비스를 지정할 수 있습니다. 기존 `carrier` + `service_name` 요청도 계속 지원합니다. 코드와 운송사·서비스명을 함께 보낼 때는 같은 서비스를 가리켜야 하며, 충돌하면 422 `SERVICE_SELECTION_CONFLICT`입니다. 잘못된 코드 형식은 400, 없거나 비활성인 Ship 코드는 422 `CARRIER_SERVICE_NOT_ACTIVE`입니다. 기존 이름이 여러 서비스와 일치하면 422 `CARRIER_SERVICE_AMBIGUOUS`이므로 Rate에서 코드를 선택하세요.

Rate에 선택한 `service_code`를 보내면 해당 서비스만 다시 조회합니다. 선택할 수 없는 코드는 422 `CARRIER_SERVICE_NOT_CUSTOMER_SELECTABLE`이며 다른 서비스로 대체하지 않습니다. 아래 코드는 합성 값이므로 실제 견적의 코드로 바꾸세요. 생성·상세·외부 참조·FKTG AWB 조회·PATCH 응답에서 같은 서비스 코드를 확인합니다. 과거 발송물의 서비스 연결이 없으면 `null`입니다. 허용 상태에서 PATCH로 코드를 변경할 수 있고, 상태·포장·목적지·incoterms·PID 검증은 그대로 적용됩니다.

### Ship: `POST /shipments`

Ship 서버가 입력 중량·규격·목적지·incoterms·신고 Item과 선택 서비스로 운임을 계산해 발송물과 함께 저장합니다. 금액을 직접 보내거나 계산을 건너뛰지 마세요. 생성·상세·PATCH의 `estimated_rate`를 저장하고 고객에게 보여주세요. 필드는 `currency`, `total_cost`, `charged_weight`, `requires_carrier_confirmation`, `dimensions_complete`, `calculated_at`입니다. 이는 계산 시점의 신고값 기준 예상 운임이며 최종 결제액이 아닙니다. 규격이 없는 기존 요청은 `dimensions_complete: false`이고 임의 규격으로 채우지 않습니다.

운임에 영향을 주는 PATCH는 운임도 함께 갱신합니다. `422 RATE_UNAVAILABLE`이면 생성·수정이 저장되지 않으므로 서비스와 입력값을 확인하고 Rate를 재조회합니다. 메모만 바꾸는 요청은 기존 운임을 유지합니다. 조회는 재계산하지 않으며 과거 계산 기록이 없으면 `estimated_rate: null`입니다. 이때 0원으로 표시하지 마세요. 등록 시 계산은 `shipments.write` 권한 안에서 처리되며 별도 `rates.read`는 서비스별 비교 요청에 필요합니다.

Yun은 발송물당 포장물 한 개입니다. 여러 포장물을 보내면 `422 CARRIER_MULTI_PIECE_UNSUPPORTED`이므로 고객이 포장물별 등록 또는 다른 서비스를 선택하게 하세요. 등록을 성공시키려고 중량·사이즈를 합치거나 품목을 임의 배분하지 않습니다. 과거 Ship의 다중 포장물 등록 허용은 운임·라벨 발행 지원을 뜻하지 않았습니다.

```json
{
  "external_order_id": "SYNTHETIC-ORDER-1",
  "external_shipment_ref": "SYNTHETIC-ORDER-1-P1",
  "service_code": "FKTG-SVC-0123456789ABCDEF0123456789ABCDEF",
  "incoterms": "DAP",
  "export_reason": "SALE",
  "sender": { "company_name": "Example Shop", "name": "Test Sender", "phone": "01000000000", "address_line1": "Synthetic Seoul address" },
  "recipient": { "name": "Test Recipient", "phone": "+10000000000", "address_line1": "Synthetic address", "city": "New York", "state": "NY", "postal_code": "10001", "country": "US" },
  "packages": [{ "package_index": 1, "weight": 1.2, "length": 25, "width": 15, "height": 10 }],
  "items": [{ "sku": "TEST-SKU-1", "name_kr": "테스트 상품", "name_en": "Test product", "quantity": 1, "unit_price": 10, "invoice_currency": "USD", "origin_country": "KR" }]
}
```

### 목재 상자 견적

```json
{
  "weight": 10,
  "incoterms": "DAP",
  "dimensions": { "length": 50, "width": 40, "height": 30 },
  "recipient": { "city": "New York", "state": "NY", "country": "US", "postal_code": "10001" },
  "declared_packaging_type": "CRATE"
}
```

### 팔레트 견적

```json
{
  "weight": 80,
  "incoterms": "DAP",
  "dimensions": { "length": 120, "width": 100, "height": 80 },
  "recipient": { "city": "New York", "state": "NY", "country": "US", "postal_code": "10001" },
  "packages": [{ "package_index": 1, "weight": 80, "length": 120, "width": 100, "height": 80, "declared_packaging_type": "PALLET", "declared_pallet_piece_count": 12 }]
}
```

### 스키드 생성

```json
{
  "external_order_id": "SYNTHETIC-SKID-1",
  "external_shipment_ref": "SYNTHETIC-SKID-1-P1",
  "carrier": "UPS",
  "service_name": "Worldwide Express Freight",
  "incoterms": "DAP",
  "export_reason": "SALE",
  "sender": { "company_name": "Example Shop", "name": "Test Sender", "phone": "01000000000", "address_line1": "Synthetic Seoul address" },
  "recipient": { "name": "Test Recipient", "phone": "+10000000000", "address_line1": "Synthetic address", "city": "New York", "state": "NY", "postal_code": "10001", "country": "US" },
  "packages": [{ "package_index": 1, "weight": 80, "length": 120, "width": 100, "height": 80, "declared_packaging_type": "SKID", "declared_pallet_piece_count": 12 }],
  "items": [{ "sku": "TEST-SKU-1", "name_kr": "테스트 상품", "name_en": "Test product", "quantity": 24, "unit_price": 10, "invoice_currency": "USD", "origin_country": "KR" }]
}
```

앞의 일반 박스 예제는 유형 생략 시 `BOX`를 사용합니다. 명시적인 `BOX`를 포함한 네 유형의 견적·생성 예제는 OpenAPI와 Postman에 있습니다. 수량 12는 합성 값이며 실제 적재 수량을 사용하세요.

## 테스트 체크리스트

- 합성 fixture와 발급된 토큰·응답 값으로 token → quote → create → detail → PATCH → AWB/PDF → tracking → 허용된 cancel을 검증합니다.
- 같은 회사의 서로 다른 credential로 동시 중복 생성, timeout 후 참조 대사를 확인합니다. 다른 회사의 ID/참조/AWB 조회는 404여야 합니다.
- 부족한 scope와 만료·비활성 credential을 검증하고, 업무 code와 correlation ID를 확인합니다.
- EU PID의 누락, placeholder, 길이, 중복, 선행 0, 빈 S-PID를 검증하고 GET→PATCH→GET으로 보존 여부를 확인합니다.
- 포장 유형 생략 시 BOX, 네 유형의 명시 입력, PATCH 유지·null·새 포장물, Rate 배열 우선순위, 적재 수량 누락·범위·합계와 UPS 서비스 불일치를 검증합니다.
- 입고스캔과 수정·취소의 경합, DB 중간 실패 시 rollback, 운송사에 전달할 mock payload를 검증합니다.
- 한국어·일본어·중국어 긴 주소와 다중 패키지가 있는 PDF를 실제로 렌더링해 확인합니다.

단일 전달 파일은 가이드와 OpenAPI에서 자동 생성하므로 직접 수정하지 않습니다. 단일 파일/HTML/공개 사본은 `node scripts/generate-customer-oms-docs.mjs`, PDF는 `python3 scripts/generate-customer-oms-guide-pdf.py`로 생성하고 `tsx --test src/lib/open-api/documentation-contract.test.ts`로 계약을 검증합니다. 문서와 로컬 테스트만으로 계정 권한·운영 검증·실제 운송사 수락을 보증하지 않습니다.

---

## OpenAPI 전체 계약

아래 YAML은 customer-oms-shipment-openapi.yaml 원본 전체입니다. 경로·필드·권한·요청과 응답 schema를 구현할 때 이 계약을 사용하세요. 예제의 인증 값은 실제 키가 아닙니다.

```yaml
openapi: 3.1.0
info:
  title: FKTG Customer OMS Open API
  version: 1.2.0
  summary: Rate API, Ship API and Tracking API for customer OMS integration
  description: >-
    Customer OMS 공개 계약. 2026-10-19 운영 DB와 앱에 반영했습니다. 고객사별 권한과 실제 운송사 수락 여부는 별도 확인 대상입니다. shipment_id는 내부 UUID와
    별개의 opaque 공개 ID입니다. 중복 범위는 company_id + external_shipment_ref이며 credential 간 공유됩니다. 별도 샌드박스는 제공되지 않습니다.
    Idempotency-Key, outbound webhook, 공개 재고/피킹 API를 지원하지 않습니다.

    2026-11-05: Rate returns stable FKTG service_code values. Select one quote, then create one shipment with
    that code. Legacy carrier + service_name requests remain supported. Examples use synthetic codes; copy a
    code returned for your account instead.
servers:
  - url: https://www.fktg.co.kr
    description: Production (not a sandbox; verify deployed contract before use)
tags:
  - name: Auth
    description: Client credential based token issuance
  - name: Rate API
    description: Customer-facing multi-carrier service quote APIs
  - name: Ship API
    description: Shipment lifecycle, agent AWB lookup and FKTG house label PDF
  - name: Tracking API
    description: Stored shipment status and event history
security:
  - bearerAuth: []
paths:
  /api/open/v1/auth/token:
    post:
      tags:
        - Auth
      summary: Issue access token
      description: Issue a Bearer access token using client_id and client_secret.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenIssueRequest'
            examples:
              default:
                value:
                  client_id: hugo_client
                  client_secret: secret_example_value
      responses:
        '200':
          description: Token issued successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenIssueResponse'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Revoked credential
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: 404 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/open/v1/rates/quotes:
    post:
      tags:
        - Rate API
      summary: Quote customer-facing rates across eligible carrier services
      description: >-
        Returns all applicable carrier/service quotes for the authenticated company. Omitted
        declared_packaging_type defaults to BOX. For one handling unit, use the top-level field; when packages
        is present, each array entry takes precedence and its omitted declaration defaults to BOX, without
        inheriting the top-level declaration. PALLET/SKID requires the actual declared_pallet_piece_count for
        every unit. UPS BOX/CRATE is not eligible for Worldwide Express Freight; PALLET/SKID is not eligible
        for UPS parcel services.

        Destination-country filtering, active flags, internal service inclusion rules,

        company carrier pricing settings, margins/discounts, and peak surcharge rules are applied.

        If one carrier quote fails, successful quotes are still returned and failures are listed in
        `unavailable_quotes`.

        A YunExpress destination missing from an allowlist-only local coverage table can return a
        contracted-rate quote

        with optional `requires_carrier_confirmation: true`. Display a carrier-confirmation notice alongside
        the price;

        quote success is not proof of carrier acceptance. Acceptance is resolved when the label is issued.

        Explicit blocked destinations and other country, service, weight and customs restrictions still apply.

        Quoting never creates a YunExpress order or calls its price-trial API.


        Required credential scope: rates.read.

        Return eligible services without creating a shipment, FKTG AWB or carrier label. Compare quotes,
        select one service_code and submit one Ship create request. A quote does not reserve a price or
        guarantee carrier acceptance. Optional service_code filters to that one eligible service; unknown,
        inactive or ineligible selections return 422 CARRIER_SERVICE_NOT_CUSTOMER_SELECTABLE without falling
        back. Malformed codes return 400 INVALID_REQUEST.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - weight
                - dimensions
                - recipient
              properties:
                declared_packaging_type:
                  $ref: '#/components/schemas/DeclaredPackagingType'
                declared_pallet_piece_count:
                  $ref: '#/components/schemas/DeclaredPalletPieceCount'
                weight:
                  type: number
                  minimum: 0.001
                dimensions:
                  type: object
                  required:
                    - length
                    - width
                    - height
                  properties:
                    length:
                      type: number
                      minimum: 0.001
                    width:
                      type: number
                      minimum: 0.001
                    height:
                      type: number
                      minimum: 0.001
                recipient:
                  type: object
                  required:
                    - city
                    - country
                    - postal_code
                  properties:
                    city:
                      type: string
                    state:
                      type:
                        - string
                        - 'null'
                    country:
                      type: string
                      minLength: 2
                      maxLength: 2
                    postal_code:
                      type: string
                    company_name:
                      type:
                        - string
                        - 'null'
                packages:
                  type: array
                  description: >-
                    Per-unit declared_packaging_type (BOX, CRATE, PALLET, SKID or null) takes precedence over
                    the top-level declaration. Omission on each unit defaults to BOX; PALLET/SKID requires
                    declared_pallet_piece_count. Top-level declarations are not copied to the array.
                  items:
                    type: object
                    required:
                      - weight
                    properties:
                      declared_packaging_type:
                        $ref: '#/components/schemas/DeclaredPackagingType'
                      declared_pallet_piece_count:
                        $ref: '#/components/schemas/DeclaredPalletPieceCount'
                      package_index:
                        type: integer
                        minimum: 1
                      weight:
                        type: number
                        minimum: 0.001
                      length:
                        type: number
                        minimum: 0.001
                      width:
                        type: number
                        minimum: 0.001
                      height:
                        type: number
                        minimum: 0.001
                item_count:
                  type: integer
                  minimum: 1
                  maximum: 1000
                  description: >-
                    Required for EU27. Declared Item count: group matching HS first 6 digits + product name +
                    origin; quantity does not multiply the count. Yun local quote policy before 2026-11-01
                    KST: KRW5400/item; after: KRW9000/item (EUR3 duty + EUR2 handling at KRW1800/EUR). VAT
                    advance fees billed separately later. Existing acceptance limits unchanged; deployment and
                    account billing must be verified.
                incoterms:
                  $ref: '#/components/schemas/Incoterms'
                service_code:
                  $ref: '#/components/schemas/PublicServiceCode'
            examples:
              default:
                value:
                  weight: 1.2
                  dimensions:
                    length: 25
                    width: 15
                    height: 10
                  recipient:
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                summary: 'Ordinary box: omitted declaration defaults to BOX'
              box:
                summary: 'BOX: explicit final packaging'
                value:
                  weight: 1.2
                  dimensions:
                    length: 25
                    width: 15
                    height: 10
                  recipient:
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  incoterms: DAP
                  declared_packaging_type: BOX
              crate:
                summary: 'CRATE: explicit final packaging'
                value:
                  weight: 10
                  dimensions:
                    length: 50
                    width: 40
                    height: 30
                  recipient:
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  incoterms: DAP
                  declared_packaging_type: CRATE
              pallet:
                summary: 'PALLET: 12 individual packages on one handling unit'
                value:
                  weight: 80
                  dimensions:
                    length: 120
                    width: 100
                    height: 80
                  recipient:
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  incoterms: DAP
                  packages:
                    - package_index: 1
                      weight: 80
                      length: 120
                      width: 100
                      height: 80
                      declared_packaging_type: PALLET
                      declared_pallet_piece_count: 12
              skid:
                summary: 'SKID: 12 individual packages on one handling unit'
                value:
                  weight: 80
                  dimensions:
                    length: 120
                    width: 100
                    height: 80
                  recipient:
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  incoterms: DAP
                  packages:
                    - package_index: 1
                      weight: 80
                      length: 120
                      width: 100
                      height: 80
                      declared_packaging_type: SKID
                      declared_pallet_piece_count: 12
              selected_service:
                summary: Requote one selected service (synthetic code)
                value:
                  weight: 1.2
                  dimensions:
                    length: 25
                    width: 15
                    height: 10
                  recipient:
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  service_code: FKTG-SVC-0123456789ABCDEF0123456789ABCDEF
      responses:
        '200':
          description: Quotes resolved successfully
          content:
            application/json:
              examples:
                default:
                  value:
                    success: true
                    request:
                      weight: 1.2
                      dimensions:
                        length: 25
                        width: 15
                        height: 10
                      recipient:
                        city: New York
                        state: NY
                        country: US
                        postal_code: '10001'
                    quotes:
                      - carrier_id: example-ups
                        carrier_code: UPS
                        carrier_name: UPS
                        service_name: Worldwide Expedited
                        currency: KRW
                        total_cost: 9900
                        charged_weight: 1.2
                        volume_weight_divisor: 5000
                        service_code: FKTG-SVC-0123456789ABCDEF0123456789ABCDEF
                    unavailable_quotes:
                      - carrier_id: example-fedex
                        carrier_code: FEDEX
                        carrier_name: FedEx
                        service_name: International Priority
                        reason: Rate quote unavailable
                        service_code: FKTG-SVC-FEDCBA9876543210FEDCBA9876543210
              schema:
                type: object
                required:
                  - success
                  - request
                  - quotes
                  - unavailable_quotes
                properties:
                  success:
                    const: true
                  request:
                    type: object
                    required:
                      - weight
                      - dimensions
                      - recipient
                    properties:
                      weight:
                        type: number
                        minimum: 0.001
                      dimensions:
                        type: object
                        required:
                          - length
                          - width
                          - height
                        properties:
                          length:
                            type: number
                            minimum: 0.001
                          width:
                            type: number
                            minimum: 0.001
                          height:
                            type: number
                            minimum: 0.001
                      recipient:
                        type: object
                        required:
                          - city
                          - country
                          - postal_code
                        properties:
                          city:
                            type: string
                          state:
                            type:
                              - string
                              - 'null'
                          country:
                            type: string
                            minLength: 2
                            maxLength: 2
                          postal_code:
                            type: string
                          company_name:
                            type:
                              - string
                              - 'null'
                      packages:
                        type: array
                        items:
                          type: object
                          required:
                            - weight
                          properties:
                            package_index:
                              type: integer
                              minimum: 1
                            weight:
                              type: number
                              minimum: 0.001
                            length:
                              type: number
                              minimum: 0.001
                            width:
                              type: number
                              minimum: 0.001
                            height:
                              type: number
                              minimum: 0.001
                      item_count:
                        type: integer
                        minimum: 1
                        maximum: 1000
                        description: >-
                          Required for EU27. Declared item count, not sum of quantity; provide actual
                          declaration count.
                      incoterms:
                        $ref: '#/components/schemas/Incoterms'
                      service_code:
                        $ref: '#/components/schemas/PublicServiceCode'
                  quotes:
                    type: array
                    items:
                      $ref: '#/components/schemas/RateQuote'
                  unavailable_quotes:
                    type: array
                    items:
                      type: object
                      properties:
                        carrier_id:
                          type: string
                        carrier_code:
                          type: string
                        carrier_name:
                          type: string
                        service_name:
                          type: string
                        reason:
                          type: string
                        service_code:
                          $ref: '#/components/schemas/PublicServiceCode'
                      required:
                        - service_code
        '400':
          description: Invalid JSON or schema validation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: 404 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: No eligible services available for the requested destination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: rates.read
  /api/open/v1/shipments:
    post:
      tags:
        - Ship API
      summary: Create shipment
      description: >-
        Create a shipment in FKTG OMS. Omitted packages[].declared_packaging_type defaults to BOX, preserving
        existing ordinary-box requests. Declare CRATE, PALLET or SKID when applicable. UPS Worldwide Express
        Freight requires eligible actual packaging and declared_pallet_piece_count; default BOX compatibility
        does not make an ordinary box eligible for Freight.


        Duplicate prevention:

        - The request is treated as unique by company_id + external_shipment_ref.

        - Re-using the same external_shipment_ref returns 409 conflict.


        Policy notes:

        - packages must contain at least one package.

        - items must contain at least one item.

        - HS Code and origin country are optional.

        - sender_ioss is optional.

        - EU destinations require export_reason; EU sales to individual recipients require merchantProductId
        and nonStandardManufacturerProductId on every item.

        - standardManufacturerProductId is optional. PID fields are strings and preserve leading zeroes.

        - Unsupported carrier/service/incoterms/destination combinations return 422.


        Required credential scope: shipments.write. Use an FKTG service_code from Rate. Without a code, legacy
        carrier + service_name remains supported. If a supplied carrier or name disagrees with the code,
        return 422 SERVICE_SELECTION_CONFLICT; never silently select another service. Unknown/disabled codes
        return 422 CARRIER_SERVICE_NOT_ACTIVE, ambiguous legacy names return 422 CARRIER_SERVICE_AMBIGUOUS,
        and destination/packaging restrictions still apply. Malformed code strings return 400 INVALID_REQUEST.
        The server calculates the selected service rate from the submitted package weights/dimensions,
        recipient, incoterms and declaration items under the authenticated company pricing, then saves freight
        and its snapshot atomically with the shipment. No client-supplied amount or quote token is required.
        Internal calculation requires no extra rates.read scope. Calculation failure returns 422
        RATE_UNAVAILABLE before shipment creation; no zero-fare registration is accepted. This saves an
        estimate, not payment or release approval. Yun supports one package per shipment; multiple packages
        return422 CARRIER_MULTI_PIECE_UNSUPPORTED before any write. Older registration acceptance did not
        imply rating or label support.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateShipmentRequest'
            examples:
              selected_service:
                summary: Create once using the selected Rate service_code (synthetic code)
                value:
                  external_order_id: ORD-20260422-1001
                  external_shipment_ref: ORD-20260422-1001-1
                  external_order_ref: MALL-10000421
                  external_channel: hugo-shop
                  order_datetime: '2026-04-22T10:20:00+09:00'
                  incoterms: DAP
                  sender:
                    company_name: HUGO SHOP
                    name: 홍길동
                    phone: '01012345678'
                    email: ops@hugoshop.com
                    address_line1: Seoul, Korea
                  sender_ioss: null
                  recipient:
                    name: John Smith
                    phone: +1-302-555-1234
                    email: john@example.com
                    address_line1: 350 5th Ave
                    address_line2: Floor 10
                    address_line3: Suite 1001
                    city: New York
                    state: NY
                    postal_code: '10001'
                    country: US
                    tax_id: null
                  packages:
                    - package_index: 1
                      weight: 1.2
                      length: 25
                      width: 15
                      height: 10
                    - package_index: 2
                      weight: 2.1
                      length: 40
                      width: 20
                      height: 10
                  items:
                    - sku: SKU-001
                      name_kr: 테스트 상품
                      name_en: Test Product
                      quantity: 2
                      unit_price: 12.5
                      unit_weight: 0.5
                      origin_country: null
                      hs_code: null
                  memo: Gift wrap requested
                  service_code: FKTG-SVC-0123456789ABCDEF0123456789ABCDEF
              fedex_dap:
                value:
                  external_order_id: ORD-20260422-1001
                  external_shipment_ref: ORD-20260422-1001-1
                  external_order_ref: MALL-10000421
                  external_channel: hugo-shop
                  order_datetime: '2026-04-22T10:20:00+09:00'
                  carrier: FEDEX
                  incoterms: DAP
                  sender:
                    company_name: HUGO SHOP
                    name: 홍길동
                    phone: '01012345678'
                    email: ops@hugoshop.com
                    address_line1: Seoul, Korea
                  sender_ioss: null
                  recipient:
                    name: John Smith
                    phone: +1-302-555-1234
                    email: john@example.com
                    address_line1: 350 5th Ave
                    address_line2: Floor 10
                    address_line3: Suite 1001
                    city: New York
                    state: NY
                    postal_code: '10001'
                    country: US
                    tax_id: null
                  packages:
                    - package_index: 1
                      weight: 1.2
                      length: 25
                      width: 15
                      height: 10
                    - package_index: 2
                      weight: 2.1
                      length: 40
                      width: 20
                      height: 10
                  items:
                    - sku: SKU-001
                      name_kr: 테스트 상품
                      name_en: Test Product
                      quantity: 2
                      unit_price: 12.5
                      unit_weight: 0.5
                      origin_country: null
                      hs_code: null
                  memo: Gift wrap requested
                  service_name: International Priority
                summary: Legacy carrier + service_name request (still supported)
              eu_pid:
                value:
                  external_order_id: SYNTHETIC-20261018-1
                  external_shipment_ref: SYNTHETIC-20261018-1-P1
                  carrier: FEDEX
                  service_name: International Priority
                  incoterms: DAP
                  export_reason: SALE
                  sender:
                    company_name: Example Shop
                    name: 테스트 발송인
                    phone: '01000000000'
                    address_line1: Synthetic Seoul address
                  recipient:
                    name: Test Recipient
                    phone: '+490000000000'
                    address_line1: Synthetic Strasse 1
                    address_line2: Test floor
                    address_line3: Test room
                    city: Berlin
                    postal_code: '10115'
                    country: DE
                  packages:
                    - package_index: 1
                      weight: 1.2
                      length: 25
                      width: 15
                      height: 10
                  items:
                    - sku: TEST-SKU-1
                      name_kr: 테스트 상품
                      name_en: Test product
                      quantity: 1
                      unit_price: 10
                      invoice_currency: EUR
                      origin_country: KR
                      merchantProductId: SHOP-TEST-1
                      nonStandardManufacturerProductId: MODEL-TEST-1
                      standardManufacturerProductId: '0012345678905'
              box:
                summary: 'BOX: synthetic shipment; use an available quoted carrier/service'
                value:
                  external_order_id: SYNTHETIC-BOX-1
                  external_shipment_ref: SYNTHETIC-BOX-1-P1
                  carrier: FEDEX
                  service_name: International Priority
                  incoterms: DAP
                  export_reason: SALE
                  sender:
                    company_name: Example Shop
                    name: Test Sender
                    phone: '01000000000'
                    address_line1: Synthetic Seoul address
                  recipient:
                    name: Test Recipient
                    phone: '+10000000000'
                    address_line1: Synthetic address
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  packages:
                    - package_index: 1
                      weight: 1.2
                      length: 25
                      width: 15
                      height: 10
                      declared_packaging_type: BOX
                  items:
                    - sku: TEST-SKU-1
                      name_kr: 테스트 상품
                      name_en: Test product
                      quantity: 1
                      unit_price: 10
                      invoice_currency: USD
                      origin_country: KR
              crate:
                summary: 'CRATE: synthetic shipment; use an available quoted carrier/service'
                value:
                  external_order_id: SYNTHETIC-CRATE-1
                  external_shipment_ref: SYNTHETIC-CRATE-1-P1
                  carrier: FEDEX
                  service_name: International Priority
                  incoterms: DAP
                  export_reason: SALE
                  sender:
                    company_name: Example Shop
                    name: Test Sender
                    phone: '01000000000'
                    address_line1: Synthetic Seoul address
                  recipient:
                    name: Test Recipient
                    phone: '+10000000000'
                    address_line1: Synthetic address
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  packages:
                    - package_index: 1
                      weight: 10
                      length: 50
                      width: 40
                      height: 30
                      declared_packaging_type: CRATE
                  items:
                    - sku: TEST-SKU-1
                      name_kr: 테스트 상품
                      name_en: Test product
                      quantity: 1
                      unit_price: 10
                      invoice_currency: USD
                      origin_country: KR
              pallet:
                summary: 'PALLET: synthetic shipment; use an available quoted carrier/service'
                value:
                  external_order_id: SYNTHETIC-PALLET-1
                  external_shipment_ref: SYNTHETIC-PALLET-1-P1
                  carrier: UPS
                  service_name: Worldwide Express Freight
                  incoterms: DAP
                  export_reason: SALE
                  sender:
                    company_name: Example Shop
                    name: Test Sender
                    phone: '01000000000'
                    address_line1: Synthetic Seoul address
                  recipient:
                    name: Test Recipient
                    phone: '+10000000000'
                    address_line1: Synthetic address
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  packages:
                    - package_index: 1
                      weight: 80
                      length: 120
                      width: 100
                      height: 80
                      declared_packaging_type: PALLET
                      declared_pallet_piece_count: 12
                  items:
                    - sku: TEST-SKU-1
                      name_kr: 테스트 상품
                      name_en: Test product
                      quantity: 24
                      unit_price: 10
                      invoice_currency: USD
                      origin_country: KR
              skid:
                summary: 'SKID: synthetic shipment; use an available quoted carrier/service'
                value:
                  external_order_id: SYNTHETIC-SKID-1
                  external_shipment_ref: SYNTHETIC-SKID-1-P1
                  carrier: UPS
                  service_name: Worldwide Express Freight
                  incoterms: DAP
                  export_reason: SALE
                  sender:
                    company_name: Example Shop
                    name: Test Sender
                    phone: '01000000000'
                    address_line1: Synthetic Seoul address
                  recipient:
                    name: Test Recipient
                    phone: '+10000000000'
                    address_line1: Synthetic address
                    city: New York
                    state: NY
                    country: US
                    postal_code: '10001'
                  packages:
                    - package_index: 1
                      weight: 80
                      length: 120
                      width: 100
                      height: 80
                      declared_packaging_type: SKID
                      declared_pallet_piece_count: 12
                  items:
                    - sku: TEST-SKU-1
                      name_kr: 테스트 상품
                      name_en: Test product
                      quantity: 24
                      unit_price: 10
                      invoice_currency: USD
                      origin_country: KR
      responses:
        '201':
          description: Shipment created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateShipmentResponse'
              examples:
                default:
                  summary: Synthetic saved declared-input estimate
                  value:
                    success: true
                    shipment_id: shp_0123456789abcdef0123456789abcdef
                    fktg_awb: '483920174625'
                    external_order_id: SYNTHETIC-ORDER-1
                    external_shipment_ref: SYNTHETIC-ORDER-1-P1
                    status: created
                    message: 발송물이 성공적으로 생성되었습니다.
                    service_code: FKTG-SVC-0123456789ABCDEF0123456789ABCDEF
                    estimated_rate:
                      currency: KRW
                      total_cost: 42000
                      charged_weight: 3
                      requires_carrier_confirmation: false
                      dimensions_complete: true
                      calculated_at: '2026-11-05T00:00:00.000Z'
        '400':
          description: Invalid JSON or schema validation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: 404 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Duplicate external_shipment_ref
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                duplicate_external_shipment_ref:
                  value:
                    success: false
                    error:
                      code: DUPLICATE_EXTERNAL_SHIPMENT_REF
                      message: The external shipment reference already exists.
                      retryable: false
                      guide: 기존 발송물을 조회하거나 external_shipment_ref를 새로운 값으로 변경해주세요.
                      field: external_shipment_ref
                      details:
                        shipment_id: shp_0123456789abcdef0123456789abcdef
                        fktg_awb: '483920174625'
                        external_order_id: ORD-20260422-1001
                        external_shipment_ref: ORD-20260422-1001-1
                        status: created
        '422':
          description: Unsupported carrier/service/incoterms policy or unknown carrier service
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                rate_unavailable:
                  summary: Selected service cannot be priced; no shipment change is saved
                  value:
                    success: false
                    error:
                      code: RATE_UNAVAILABLE
                      message: >-
                        A rate for the selected service could not be calculated. Retry the rate request before
                        saving the shipment.
                      retryable: false
                      correlation_id: 00000000-0000-4000-8000-000000000001
                yun_multi_piece:
                  summary: Yun multi-piece registration is unsupported
                  value:
                    success: false
                    error:
                      code: CARRIER_MULTI_PIECE_UNSUPPORTED
                      message: Yun supports one package per shipment.
                      retryable: false
                      correlation_id: 00000000-0000-4000-8000-000000000001
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                shipment_persistence_failed:
                  summary: The shipment could not be saved; retrying the same payload will not help
                  value:
                    success: false
                    error:
                      correlation_id: 2b7c1a90-5e4d-4b3a-8f61-0d9c4e2a7b53
                      code: SHIPMENT_PERSISTENCE_FAILED
                      message: Failed to save the shipment.
                      retryable: false
                internal_error:
                  summary: Unexpected server failure; keep the correlation ID for support
                  value:
                    success: false
                    error:
                      correlation_id: 6a0d8c31-2f9b-4e57-a1c8-3d7f5b9e0a24
                      code: INTERNAL_ERROR
                      message: Failed to create shipment.
                      retryable: false
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                awb_generation_failed:
                  summary: FKTG AWB generation failed; the request itself is valid, retry after the advertised delay
                  value:
                    success: false
                    error:
                      correlation_id: 4c9d3b1e-8f7a-4c2d-9e51-6b0a7f2d3c88
                      code: AWB_GENERATION_FAILED
                      message: Failed to generate FKTG AWB. Please retry.
                      retryable: true
                service_unavailable:
                  summary: Transient database/connection failure; safe to retry with backoff
                  value:
                    success: false
                    error:
                      correlation_id: 9e2f6a54-1d3b-4f80-8c47-2a5b9e0d7f16
                      code: SERVICE_UNAVAILABLE
                      message: The service is temporarily unavailable. Please retry.
                      retryable: true
      x-required-scope: shipments.write
  /api/open/v1/shipments/{shipmentId}:
    get:
      tags:
        - Ship API
      summary: Get shipment by shipment ID
      parameters:
        - $ref: '#/components/parameters/ShipmentId'
      responses:
        '200':
          description: Shipment found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShipmentDetailResponse'
              examples:
                default:
                  value:
                    success: true
                    shipment_id: shp_0123456789abcdef0123456789abcdef
                    fktg_awb: '123456789012'
                    carrier_awb: null
                    external_order_id: ORDER
                    external_shipment_ref: REF
                    status: created
                    carrier: null
                    carrier_name: null
                    service_name: null
                    export_reason: SALE
                    recipient:
                      name: Synthetic Name
                      company_name: null
                      phone: '000'
                      email: null
                      address_line1: Synthetic address
                      address_line2: null
                      address_line3: null
                      city: Berlin
                      state: null
                      postal_code: '10115'
                      country: DE
                    package_summary:
                      package_count: 1
                      actual_weight: 1
                      billable_weight: 1
                      packages:
                        - package_index: 1
                          declared_packaging_type: null
                          declared_pallet_piece_count: null
                          packaging_type: null
                          pallet_piece_count: null
                          weight: 1
                          billable_weight: 1
                          length: 10
                          width: 10
                          height: 10
                    items:
                      - sku: SKU
                        name_kr: 테스트
                        name_en: Test
                        quantity: 1
                        unit_price: 1
                        invoice_currency: EUR
                        unit_weight: 1
                        origin_country: KR
                        origin_country_code: KR
                        hs_code: '610910'
                        merchantProductId: SHOP-001
                        nonStandardManufacturerProductId: MODEL-001
                        standardManufacturerProductId: '0012345678905'
                    timestamps:
                      created_at: null
                      updated_at: null
                      status_updated_at: null
                      external_updated_at: null
                    service_code: null
                    estimated_rate: null
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.read
      description: |-

        Required credential scope: shipments.read.
    patch:
      tags:
        - Ship API
      summary: Update shipment by shipment ID
      description: >-
        Shipment update is allowed only before the shipment reaches FKTG warehouse inbound scan. Packages is a
        full replacement. For a matching package_index, an omitted declared_packaging_type preserves the saved
        declaration; for a new package, omission defaults to BOX. Explicit null clears the declaration.
        Warehouse-confirmed packaging_type and pallet_piece_count are read-only.

        Once the shipment enters `received_at_warehouse` or later equivalent state, updates are rejected.


        Required credential scope: shipments.write. Use an FKTG service_code from Rate. Without a code, legacy
        carrier + service_name remains supported. If a supplied carrier or name disagrees with the code,
        return 422 SERVICE_SELECTION_CONFLICT; never silently select another service. Unknown/disabled codes
        return 422 CARRIER_SERVICE_NOT_ACTIVE, ambiguous legacy names return 422 CARRIER_SERVICE_AMBIGUOUS,
        and destination/packaging restrictions still apply. Malformed code strings return 400 INVALID_REQUEST.
        Omitted selection fields retain the existing selection behavior. State, PID, packaging and incoterms
        validation still apply. Price-affecting changes recalculate freight from effective saved/new inputs.
        Shipment changes, price and snapshot commit together after locking and checking updated_at; concurrent
        edits return 409 and require re-fetch. Rate failure leaves previous data and freight unchanged.
        Metadata-only changes retain the saved estimate. Yun supports one package per shipment; multiple
        packages return422 CARRIER_MULTI_PIECE_UNSUPPORTED before any write. Older registration acceptance did
        not imply rating or label support.
      parameters:
        - $ref: '#/components/parameters/ShipmentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateShipmentRequest'
            examples:
              recipient_and_packages:
                value:
                  incoterms: DDP
                  sender_ioss: IM2760000742
                  recipient:
                    phone: +1-302-555-5678
                    address_line1: 11 Madison Ave
                    city: New York
                    state: NY
                    postal_code: '10010'
                    country: US
                  packages:
                    - package_index: 1
                      weight: 1.4
                      length: 30
                      width: 18
                      height: 12
                  items:
                    - sku: SKU-001
                      name_kr: 테스트 상품
                      name_en: Test Product
                      quantity: 2
                      unit_price: 12.5
                      unit_weight: 0.5
                      origin_country: null
                      hs_code: null
              eu_pid_full_replacement:
                value:
                  export_reason: SALE
                  items:
                    - sku: TEST-SKU-1
                      name_kr: 테스트 상품
                      name_en: Test product
                      quantity: 2
                      unit_price: 10
                      invoice_currency: EUR
                      merchantProductId: SHOP-TEST-1
                      nonStandardManufacturerProductId: MODEL-TEST-1
                      standardManufacturerProductId: '0012345678905'
              selected_service:
                summary: Change service before inbound/carrier label lock (synthetic code)
                value:
                  service_code: FKTG-SVC-0123456789ABCDEF0123456789ABCDEF
      responses:
        '200':
          description: Shipment updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShipmentDetailResponse'
        '400':
          description: Invalid JSON or schema validation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Shipment can no longer be updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                not_editable:
                  value:
                    success: false
                    error:
                      code: SHIPMENT_NOT_EDITABLE
                      message: Shipment update is not allowed after warehouse inbound scan.
                      guide: FKTG 센터 입고스캔 전까지만 수정 가능합니다.
        '422':
          description: Unsupported carrier/service/incoterms policy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                rate_unavailable:
                  summary: Selected service cannot be priced; no shipment change is saved
                  value:
                    success: false
                    error:
                      code: RATE_UNAVAILABLE
                      message: >-
                        A rate for the selected service could not be calculated. Retry the rate request before
                        saving the shipment.
                      retryable: false
                      correlation_id: 00000000-0000-4000-8000-000000000001
                yun_multi_piece:
                  summary: Yun multi-piece registration is unsupported
                  value:
                    success: false
                    error:
                      code: CARRIER_MULTI_PIECE_UNSUPPORTED
                      message: Yun supports one package per shipment.
                      retryable: false
                      correlation_id: 00000000-0000-4000-8000-000000000001
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.write
  /api/open/v1/shipments/external/{externalShipmentRef}:
    get:
      tags:
        - Ship API
      summary: Get shipment by external shipment reference
      parameters:
        - $ref: '#/components/parameters/ExternalShipmentRef'
      responses:
        '200':
          description: Shipment found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShipmentDetailResponse'
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.read
      description: |-

        Required credential scope: shipments.read.
  /api/open/v1/shipments/fktg-awb/{fktgAwb}:
    get:
      tags:
        - Ship API
      summary: Get shipment by FKTG AWB
      parameters:
        - $ref: '#/components/parameters/FktgAwb'
      responses:
        '200':
          description: Shipment found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShipmentDetailResponse'
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.read
      description: |-

        Required credential scope: shipments.read.
  /api/open/v1/shipments/{shipmentId}/cancel:
    post:
      tags:
        - Ship API
      summary: Cancel shipment
      description: |-
        Shipment cancel is allowed only before the shipment reaches FKTG warehouse inbound scan.

        Required credential scope: shipments.write.
      parameters:
        - $ref: '#/components/parameters/ShipmentId'
      responses:
        '200':
          description: Shipment cancelled successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CancelShipmentResponse'
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Shipment can no longer be cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                not_cancellable:
                  value:
                    success: false
                    error:
                      code: SHIPMENT_NOT_CANCELLABLE
                      message: Shipment cancel is not allowed after warehouse inbound scan.
                      guide: FKTG 센터 입고스캔 전까지만 취소 가능합니다.
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.write
  /api/open/v1/shipments/{shipmentId}/tracking:
    get:
      tags:
        - Tracking API
      summary: Get tracking events
      parameters:
        - $ref: '#/components/parameters/ShipmentId'
      responses:
        '200':
          description: Tracking payload returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrackingResponse'
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.read
      description: |-

        Required credential scope: shipments.read.
  /api/open/v1/shipments/{shipmentId}/awb:
    get:
      tags:
        - Ship API
      summary: Get FKTG and agent waybill numbers
      parameters:
        - $ref: '#/components/parameters/ShipmentId'
      responses:
        '200':
          description: AWB data returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AwbResponse'
              examples:
                assigned:
                  value:
                    success: true
                    shipment_id: shp_0123456789abcdef0123456789abcdef
                    fktg_awb: '123456789012'
                    carrier_awb: '001234567890'
                    external_order_id: SYNTHETIC-ORDER-1
                    external_shipment_ref: SYNTHETIC-ORDER-1-P1
                    status: carrier_label_printed
                awaiting_assignment:
                  value:
                    success: true
                    shipment_id: shp_0123456789abcdef0123456789abcdef
                    fktg_awb: '123456789012'
                    carrier_awb: null
                    external_order_id: SYNTHETIC-ORDER-1
                    external_shipment_ref: SYNTHETIC-ORDER-1-P1
                    status: created
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.read
      description: >-
        Ship API returns the saved FKTG number (fktg_awb) and agent/carrier number (carrier_awb).

        carrier_awb is null until assigned and saved; refetch this endpoint to obtain it later.

        Preserve both values as strings, including leading zeros. This GET does not issue a carrier label.

        Required credential scope: shipments.read. Only shipments belonging to the credential's company are
        accessible.
  /api/open/v1/shipments/{shipmentId}/label:
    get:
      tags:
        - Ship API
      summary: Download FKTG label PDF
      parameters:
        - $ref: '#/components/parameters/ShipmentId'
      responses:
        '200':
          description: PDF blob response
          headers:
            Content-Type:
              schema:
                type: string
                example: application/pdf
            Content-Disposition:
              schema:
                type: string
                example: inline; filename="483920174625.pdf"
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '400':
          description: 400 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: 403 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Shipment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 409 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 422 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: 429 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 500 error; see error.code and correlation_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            503 error; retryable=true. Honour Retry-After, then retry the same payload with backoff. See
            error.code for the fixed server code and correlation_id for support
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-required-scope: shipments.read
      description: |-

        Required credential scope: shipments.read.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Opaque token; never decode it or infer permissions. Use the issued scope string.
  parameters:
    ShipmentId:
      name: shipmentId
      in: path
      required: true
      description: Stable opaque public shipment ID, never an internal database UUID
      schema:
        type: string
        minLength: 1
        pattern: ^shp_[a-f0-9]{32}$
      example: shp_0123456789abcdef0123456789abcdef
    ExternalShipmentRef:
      name: externalShipmentRef
      in: path
      required: true
      description: Company-scoped external reference (shared across credentials)
      schema:
        type: string
        minLength: 1
      example: ORD-20260422-1001-1
    FktgAwb:
      name: fktgAwb
      in: path
      required: true
      description: 12-digit FKTG AWB
      schema:
        type: string
        pattern: ^[0-9]{12}$
      example: '483920174625'
  schemas:
    TokenIssueRequest:
      type: object
      required:
        - client_id
        - client_secret
      properties:
        client_id:
          type: string
          minLength: 1
        client_secret:
          type: string
          minLength: 1
    TokenIssueResponse:
      type: object
      required:
        - access_token
        - token_type
        - expires_in
        - scope
      properties:
        access_token:
          type: string
        token_type:
          type: string
          enum:
            - Bearer
        expires_in:
          type: integer
          minimum: 1
          example: 3600
        scope:
          type: string
          description: >-
            Space-separated permissions actually granted to this credential. Empty means no scopes; not all
            credentials have all permissions.
    Sender:
      type: object
      required:
        - company_name
        - name
        - phone
        - address_line1
      properties:
        company_name:
          type: string
        name:
          type: string
        phone:
          type: string
        email:
          type:
            - string
            - 'null'
          format: email
        address_line1:
          type: string
    Recipient:
      type: object
      required:
        - name
        - phone
        - address_line1
        - city
        - postal_code
        - country
      properties:
        name:
          type: string
        phone:
          type: string
        email:
          type:
            - string
            - 'null'
          format: email
        address_line1:
          type: string
        address_line2:
          type:
            - string
            - 'null'
        address_line3:
          type:
            - string
            - 'null'
        city:
          type: string
        state:
          type:
            - string
            - 'null'
        postal_code:
          type: string
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO 3166-1 alpha-2 recommended
        tax_id:
          type:
            - string
            - 'null'
        company_name:
          type:
            - string
            - 'null'
          description: >-
            Current software uses a nonempty company name for B2B classification; this is not a legal
            classification.
    DeclaredPackagingType:
      type:
        - string
        - 'null'
      enum:
        - BOX
        - CRATE
        - PALLET
        - SKID
        - null
      description: >-
        Sender-declared final handling unit. BOX means a paper carton or customer packaging (MY PACKAGE);
        CRATE means a wooden box; PALLET means a pallet; SKID means a skid. MY_PACKAGE is not an input value.
        Omission defaults to BOX on creation and rate quotes; existing ordinary-box requests need no new
        field. For rate quotes, packages entries take precedence over top-level declarations and do not
        inherit them. On PATCH, omission preserves the saved declaration for the same package_index and
        defaults to BOX for a new package; explicit null clears it. Legacy or explicitly cleared declarations
        can read back as null. Separate from warehouse-confirmed packaging_type. UPS BOX/CRATE cannot use
        Worldwide Express Freight; PALLET/SKID cannot use UPS parcel services. Each PALLET/SKID needs
        declared_pallet_piece_count. An incompatible service returns HTTP 422
        CARRIER_SERVICE_NOT_CUSTOMER_SELECTABLE or INVALID_PACKAGING_SERVICE depending on the validation
        stage; a provider may return no quote.
    DeclaredPalletPieceCount:
      type:
        - integer
        - 'null'
      minimum: 1
      maximum: 99999
      description: >-
        Number of individual packages loaded on this pallet or skid, not the number of pallets or product
        units. Required for PALLET/SKID and UPS Worldwide Express Freight, including legacy rows without a
        declared type. Each handling unit requires its own count; no automatic default is applied. The total
        cannot exceed 99999. On package PATCH, omission preserves the saved count; null clears it only when
        the effective service and packaging do not require it.
    ShipmentPackage:
      type: object
      required:
        - package_index
        - weight
      properties:
        declared_packaging_type:
          $ref: '#/components/schemas/DeclaredPackagingType'
        declared_pallet_piece_count:
          $ref: '#/components/schemas/DeclaredPalletPieceCount'
        package_index:
          type: integer
          minimum: 1
        weight:
          type: number
          exclusiveMinimum: 0
        length:
          type:
            - number
            - 'null'
          minimum: 0
        width:
          type:
            - number
            - 'null'
          minimum: 0
        height:
          type:
            - number
            - 'null'
          minimum: 0
    ShipmentItem:
      type: object
      required:
        - name_kr
        - name_en
        - quantity
        - unit_price
      properties:
        sku:
          type:
            - string
            - 'null'
        name_kr:
          type: string
        name_en:
          type: string
        quantity:
          type: integer
          minimum: 1
        unit_price:
          type: number
          minimum: 0
        unit_weight:
          type:
            - number
            - 'null'
          minimum: 0
        origin_country:
          type:
            - string
            - 'null'
          description: >-
            Optional ISO alpha-2. Same-value aliases accepted; conflicting origin_country_code rejected. Both
            readback aliases represent the stored origin.
        hs_code:
          type:
            - string
            - 'null'
          description: Optional. Additional customs info may be requested later.
        invoice_currency:
          type: string
          enum:
            - USD
            - KRW
            - JPY
            - CNY
            - GBP
            - EUR
          default: USD
        origin_country_code:
          type:
            - string
            - 'null'
          description: >-
            Alias for origin_country; when both nonempty values are supplied they must agree
            (case-insensitive).
    ProductIdentifier:
      type:
        - string
        - 'null'
      maxLength: 200
      description: >-
        Printable ASCII identifier or comma-separated identifiers. Each identifier must contain 2-70
        characters; the entire field is limited to 200 characters. Leading zeroes are preserved. Duplicate
        identifiers and placeholders such as N/A, NA, NULL, NONE, UNKNOWN, all-zero or all-nine values are
        rejected.
      example: 001234,MODEL-02
    CreateShipmentItem:
      allOf:
        - $ref: '#/components/schemas/ShipmentItem'
        - type: object
          properties:
            merchantProductId:
              allOf:
                - $ref: '#/components/schemas/ProductIdentifier'
              description: >-
                M-PID. Required for EU SALE shipments to individual recipients. Shipper-assigned product
                identifier; use Amazon ASIN/FNSKU or the platform/store product-management SKU. May equal
                NS-PID; supply both fields explicitly.
            nonStandardManufacturerProductId:
              allOf:
                - $ref: '#/components/schemas/ProductIdentifier'
              description: >-
                NS-PID. Required for EU SALE shipments to individual recipients. Shipper-assigned product
                identifier; use Amazon ASIN/FNSKU or the platform/store product-management SKU. May equal
                M-PID; never auto-filled from it.
            standardManufacturerProductId:
              allOf:
                - $ref: '#/components/schemas/ProductIdentifier'
              description: >-
                S-PID. Optional international-standard barcode assigned by the manufacturer or brand, such as
                GTIN, EAN, UPC or ISBN. Preserve leading zeroes as a string.
    CreateShipmentRequest:
      type: object
      required:
        - external_order_id
        - external_shipment_ref
        - incoterms
        - sender
        - recipient
        - packages
        - items
      properties:
        external_order_id:
          type: string
        external_shipment_ref:
          type: string
          description: Unique per company scope
        external_order_ref:
          type:
            - string
            - 'null'
        external_channel:
          type:
            - string
            - 'null'
        order_datetime:
          type:
            - string
            - 'null'
          format: date-time
        carrier:
          type: string
          example: FEDEX
        incoterms:
          $ref: '#/components/schemas/Incoterms'
        sender:
          $ref: '#/components/schemas/Sender'
        sender_ioss:
          type:
            - string
            - 'null'
          description: Optional. Useful for EU destinations.
        export_reason:
          type: string
          enum:
            - SALE
            - GIFT
            - SAMPLE
            - RETURN
            - REPAIR
            - INTERCOMPANYDATA
            - PERSONAL_EFFECTS
            - OTHER
          description: >-
            Required for EU destinations. Use the actual purpose; missing EU purpose returns 422
            INVALID_EU_EXPORT_REASON.
        recipient:
          $ref: '#/components/schemas/Recipient'
        packages:
          type: array
          minItems: 1
          description: >-
            Each final handling unit accepts declared_packaging_type: BOX (paper carton/customer packaging, MY
            PACKAGE), CRATE (wooden box), PALLET or SKID; explicit null is allowed. Omission defaults to BOX.
            PALLET/SKID requires declared_pallet_piece_count.
          items:
            $ref: '#/components/schemas/ShipmentPackage'
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/CreateShipmentItem'
        memo:
          type:
            - string
            - 'null'
        service_name:
          type: string
          description: >-
            Legacy selection: exact Rate service_name together with carrier. Optional when service_code is
            provided; if supplied, it must match that code.
          example: International Priority
        service_code:
          $ref: '#/components/schemas/PublicServiceCode'
          description: >-
            Use the selected Rate quote service_code. Without a code, both carrier and service_name are
            required. Supplied carrier/name must agree with the code.
      anyOf:
        - required:
            - service_code
        - required:
            - carrier
            - service_name
    PartialSender:
      type: object
      minProperties: 1
      properties:
        company_name:
          type: string
        name:
          type: string
        phone:
          type: string
        email:
          type:
            - string
            - 'null'
          format: email
        address_line1:
          type: string
    PartialRecipient:
      type: object
      minProperties: 1
      properties:
        name:
          type: string
        phone:
          type: string
        email:
          type:
            - string
            - 'null'
          format: email
        address_line1:
          type: string
        address_line2:
          type:
            - string
            - 'null'
        address_line3:
          type:
            - string
            - 'null'
        city:
          type: string
        state:
          type:
            - string
            - 'null'
        postal_code:
          type: string
        country:
          type: string
          minLength: 2
          maxLength: 2
        tax_id:
          type:
            - string
            - 'null'
        company_name:
          type:
            - string
            - 'null'
          description: >-
            Current software uses a nonempty company name for B2B classification; this is not a legal
            classification.
    UpdateShipmentRequest:
      type: object
      minProperties: 1
      description: >-
        Partial shipment update; items and packages, when supplied, replace the complete arrays. Send every
        item including its PIDs. SKU or array order never identifies an old item. DB transaction locks parent,
        rechecks editable state, replaces children and binds PID snapshot to new identities. Omitted fields
        remain unchanged. Issued carrier label or warehouse scan blocks update.
      properties:
        external_order_ref:
          type:
            - string
            - 'null'
        external_channel:
          type:
            - string
            - 'null'
        carrier:
          type: string
        incoterms:
          $ref: '#/components/schemas/Incoterms'
        memo:
          type:
            - string
            - 'null'
        sender_ioss:
          type:
            - string
            - 'null'
        sender:
          $ref: '#/components/schemas/PartialSender'
        recipient:
          $ref: '#/components/schemas/PartialRecipient'
        packages:
          type: array
          minItems: 1
          description: >-
            Full replacement. declared_packaging_type accepts BOX, CRATE, PALLET, SKID or null. At the same
            package_index, omission preserves the saved declaration; omission on a new unit defaults to BOX.
            Explicit null clears the declaration.
          items:
            $ref: '#/components/schemas/ShipmentPackage'
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/CreateShipmentItem'
        service_name:
          type: string
        export_reason:
          type: string
          enum:
            - SALE
            - GIFT
            - SAMPLE
            - RETURN
            - REPAIR
            - INTERCOMPANYDATA
            - PERSONAL_EFFECTS
            - OTHER
          description: >-
            Required for EU destinations. Use the actual purpose; missing EU purpose returns 422
            INVALID_EU_EXPORT_REASON.
        service_code:
          $ref: '#/components/schemas/PublicServiceCode'
          description: >-
            Select another eligible service before the shipment becomes immutable. Omission preserves existing
            selection behavior. Supplied carrier/name must agree with the code.
    CreateShipmentResponse:
      type: object
      required:
        - success
        - shipment_id
        - fktg_awb
        - external_order_id
        - external_shipment_ref
        - status
        - message
        - service_code
        - estimated_rate
      properties:
        success:
          type: boolean
          const: true
        shipment_id:
          type: string
          pattern: ^shp_[a-f0-9]{32}$
        fktg_awb:
          type: string
          pattern: ^[0-9]{12}$
        external_order_id:
          type: string
        external_shipment_ref:
          type: string
        status:
          type: string
          example: created
        message:
          type: string
          example: Shipment created successfully.
        service_code:
          $ref: '#/components/schemas/PublicServiceCode'
        estimated_rate:
          $ref: '#/components/schemas/ShipmentEstimatedRate'
    CancelShipmentResponse:
      type: object
      required:
        - success
        - shipment_id
        - fktg_awb
        - external_order_id
        - external_shipment_ref
        - status
        - message
      properties:
        success:
          type: boolean
          const: true
        shipment_id:
          type: string
          pattern: ^shp_[a-f0-9]{32}$
        fktg_awb:
          type: string
          pattern: ^[0-9]{12}$
        external_order_id:
          type: string
        external_shipment_ref:
          type: string
        status:
          type: string
          const: cancelled
        message:
          type: string
          example: Shipment cancelled successfully.
    ShipmentStatus:
      type: string
      enum:
        - created
        - pending_classification
        - ready_for_label
        - label_printed
        - carrier_label_printed
        - received_at_warehouse
        - shipped
        - in_transit
        - customs
        - delivered
        - returned
        - cancelled
        - exception
        - delivery_failed
    Incoterms:
      type: string
      enum:
        - DAP
        - DDP
      description: >-
        FedEx DAP/DDP; UPS DAP/DDP; YUN BR DAP only, all other destinations DDP; SF US DDP only, flexible
        services DAP/DDP elsewhere, E-Commerce Express CD DAP only for TH/TW/HK/CN/SG/VN/MO/PH and DDP
        otherwise. Invalid combinations rejected; no silent payer change. Existing create compatibility: Yun
        non-BR and some SF-CD DAP requests normalize to DDP with explicit INCOTERMS_NORMALIZED_TO_DDP warning.
        Obtain payer consent and request DDP explicitly. Quotes do not normalize. PATCH rejects invalid
        incoterms with 422; it does not normalize DAP to DDP.
    PackageSummaryPackage:
      type: object
      properties:
        declared_packaging_type:
          $ref: '#/components/schemas/DeclaredPackagingType'
        declared_pallet_piece_count:
          $ref: '#/components/schemas/DeclaredPalletPieceCount'
        pallet_piece_count:
          type:
            - integer
            - 'null'
          minimum: 1
          maximum: 99999
          readOnly: true
          description: >-
            Warehouse-confirmed count of packages loaded on this pallet or skid. Customers cannot set this
            field.
        packaging_type:
          type:
            - string
            - 'null'
          enum:
            - BOX
            - CRATE
            - PALLET
            - SKID
            - null
          readOnly: true
          description: Warehouse-confirmed packaging. Customers cannot set this field.
        package_index:
          type: integer
        weight:
          type:
            - number
            - 'null'
        billable_weight:
          type:
            - number
            - 'null'
        length:
          type:
            - number
            - 'null'
        width:
          type:
            - number
            - 'null'
        height:
          type:
            - number
            - 'null'
    PackageSummary:
      type: object
      properties:
        package_count:
          type:
            - integer
            - 'null'
        actual_weight:
          type:
            - number
            - 'null'
        billable_weight:
          type:
            - number
            - 'null'
        packages:
          type: array
          items:
            $ref: '#/components/schemas/PackageSummaryPackage'
    ShipmentTimestamps:
      type: object
      properties:
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
        status_updated_at:
          type:
            - string
            - 'null'
          format: date-time
        external_updated_at:
          type:
            - string
            - 'null'
          format: date-time
    ShipmentDetailData:
      type: object
      properties:
        shipment_id:
          type: string
          pattern: ^shp_[a-f0-9]{32}$
        fktg_awb:
          type:
            - string
            - 'null'
          pattern: ^[0-9]{12}$
        carrier_awb:
          type:
            - string
            - 'null'
        external_order_id:
          type:
            - string
            - 'null'
        external_shipment_ref:
          type:
            - string
            - 'null'
        status:
          $ref: '#/components/schemas/ShipmentStatus'
        carrier:
          type:
            - string
            - 'null'
        carrier_name:
          type:
            - string
            - 'null'
        service_name:
          type:
            - string
            - 'null'
        recipient:
          type: object
          properties:
            name:
              type: string
            phone:
              type: string
            email:
              type:
                - string
                - 'null'
              format: email
            address_line1:
              type: string
            address_line2:
              type:
                - string
                - 'null'
            address_line3:
              type:
                - string
                - 'null'
            city:
              type: string
            state:
              type:
                - string
                - 'null'
            postal_code:
              type: string
            country:
              type: string
              minLength: 2
              maxLength: 2
              description: ISO 3166-1 alpha-2 recommended
            company_name:
              type:
                - string
                - 'null'
              description: >-
                Current software uses a nonempty company name for B2B classification; this is not a legal
                classification.
        package_summary:
          $ref: '#/components/schemas/PackageSummary'
        items:
          type: array
          items:
            $ref: '#/components/schemas/CreateShipmentItem'
        timestamps:
          $ref: '#/components/schemas/ShipmentTimestamps'
        export_reason:
          type:
            - string
            - 'null'
        service_code:
          anyOf:
            - $ref: '#/components/schemas/PublicServiceCode'
            - type: 'null'
          description: >-
            FKTG code of the associated service; null for historical records with no service relation. Also
            returned by external-reference and FKTG-AWB detail lookup.
        estimated_rate:
          anyOf:
            - $ref: '#/components/schemas/ShipmentEstimatedRate'
            - type: 'null'
          description: >-
            Latest saved declared-input API estimate. Null for historical shipments with no such snapshot. GET
            does not recalculate or charge.
    ShipmentDetailResponse:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          const: true
      allOf:
        - $ref: '#/components/schemas/ShipmentDetailData'
    AwbResponse:
      type: object
      required:
        - success
        - shipment_id
        - fktg_awb
        - carrier_awb
        - status
      properties:
        success:
          type: boolean
          const: true
        shipment_id:
          type: string
          pattern: ^shp_[a-f0-9]{32}$
        fktg_awb:
          type:
            - string
            - 'null'
          pattern: ^[0-9]{12}$
        carrier_awb:
          description: >-
            Saved agent/carrier waybill number shown as Agent AWB in the portal; null until assigned. Preserve
            leading zeros.
          type:
            - string
            - 'null'
        external_order_id:
          type:
            - string
            - 'null'
        external_shipment_ref:
          type:
            - string
            - 'null'
        status:
          $ref: '#/components/schemas/ShipmentStatus'
    TrackingEvent:
      type: object
      properties:
        time:
          type:
            - string
            - 'null'
          format: date-time
        code:
          type:
            - string
            - 'null'
        description:
          type:
            - string
            - 'null'
        location:
          type:
            - string
            - 'null'
        status:
          type:
            - string
            - 'null'
    TrackingResponse:
      type: object
      required:
        - success
        - shipment_id
        - tracking_status
        - events
      properties:
        success:
          type: boolean
          const: true
        shipment_id:
          type: string
          pattern: ^shp_[a-f0-9]{32}$
        fktg_awb:
          type:
            - string
            - 'null'
          pattern: ^[0-9]{12}$
        carrier_awb:
          type:
            - string
            - 'null'
        tracking_status:
          $ref: '#/components/schemas/ShipmentStatus'
        events:
          type: array
          items:
            $ref: '#/components/schemas/TrackingEvent'
    ErrorBody:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: INVALID_REQUEST
        message:
          type: string
        guide:
          type: string
        field:
          type: string
        details:
          description: Optional error details object
        correlation_id:
          type: string
          format: uuid
          description: Server-generated support correlation ID, also returned in x-correlation-id.
        retryable:
          type: boolean
          description: >-
            Always present on error responses. true only for 429 and 503, which may succeed after the
            Retry-After window; false means the request must be changed before it can succeed. Server 5xx
            codes are a fixed enum: AWB_GENERATION_FAILED / SERVICE_UNAVAILABLE (503, retryable) and
            SHIPMENT_PERSISTENCE_FAILED / INTERNAL_ERROR (500, not retryable).
    ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          const: false
        error:
          $ref: '#/components/schemas/ErrorBody'
    RateQuote:
      type: object
      additionalProperties: false
      required:
        - carrier_id
        - carrier_code
        - carrier_name
        - service_name
        - currency
        - total_cost
        - charged_weight
        - volume_weight_divisor
        - service_code
      properties:
        carrier_id:
          type: string
        carrier_code:
          type: string
        carrier_name:
          type: string
        service_name:
          type: string
        currency:
          type: string
        total_cost:
          type: number
        charged_weight:
          type: number
        volume_weight_divisor:
          type: number
        requires_carrier_confirmation:
          type: boolean
          description: 'Optional true: rate exists but carrier acceptance must still be confirmed.'
        service_code:
          $ref: '#/components/schemas/PublicServiceCode'
    PublicServiceCode:
      type: string
      pattern: ^FKTG-SVC-[0-9A-F]{32}$
      description: >-
        Stable opaque FKTG service identifier returned by Rate API. Not a provider-native service code. Use
        the returned value verbatim; do not derive it from carrier/name or an internal ID.
      example: FKTG-SVC-0123456789ABCDEF0123456789ABCDEF
    ShipmentEstimatedRate:
      type: object
      additionalProperties: false
      description: >-
        Saved estimate calculated by the server from declared Ship inputs and company pricing. Not a settled
        charge, deposit deduction or price reservation. calculated_at identifies this historical estimate;
        final measurement and additional fees may change billing.
      required:
        - currency
        - total_cost
        - charged_weight
        - requires_carrier_confirmation
        - dimensions_complete
        - calculated_at
      properties:
        currency:
          type: string
          pattern: ^[A-Z]{3}$
        total_cost:
          type: number
          exclusiveMinimum: 0
        charged_weight:
          type: number
          exclusiveMinimum: 0
        requires_carrier_confirmation:
          type: boolean
        dimensions_complete:
          type: boolean
          description: >-
            False when any package lacks complete dimensions. The input remains compatible; volumetric
            remeasurement can increase the fare.
        calculated_at:
          type: string
          format: date-time
x-fktg-policy:
  duplicate_prevention:
    key: company_id + external_shipment_ref
  operation_window:
    update: Allowed before warehouse inbound scan
    cancel: Allowed before warehouse inbound scan
  customs_optional_fields:
    hs_code: optional
    origin_country: optional
    sender_ioss: optional
  incoterms_matrix:
    FEDEX:
      allowed:
        - DAP
        - DDP
    UPS:
      allowed:
        - DAP
    YUN:
      allowed_default:
        - DDP
      exceptions:
        - country: BR
          note: Brazil handled by exception policy; unsupported combinations are rejected.
    SF:
      INTERNATIONAL_STANDARD_EXPRESS:
        allowed:
          - DAP
          - DDP
      GLOBAL_STANDARD_PLUS:
        allowed:
          - DAP
          - DDP
      INTERNATIONAL_ECONOMY_EXPRESS:
        allowed:
          - DAP
          - DDP
      E_COMMERCE_EXPRESS_CD:
        allowed:
          - DDP
```
