---
title: "Create Product"
method: POST
path: "/v1/products"
tags: ["Product Catalog"]
---

# Create Product

`POST /v1/products`

## Request

- Base URL: `https://api.xdata.kr` (the document declares no server; this is the origin it was published from)
- URL: `https://api.xdata.kr/v1/products`
- Auth: one of:
  - HTTP basic
  - OAuth 2 (authorization code, authorization URL `https://api.xdata.kr/oauth/authorize`, token URL `https://api.xdata.kr/oauth/token`)

## Query parameters

- `env_scope` 'production' | 'real_test' | 'sandbox', nullable: 환경을 고릅니다. 값은 production · real_test · sandbox 중 하나입니다. 경로 접두(/v1/sandbox/... · /v1/test/... · /v1/production/...) · X-Env-Scope 헤더 · env_scope 쿼리 가운데 한 통로로 반드시 보내야 합니다. 빼면 자동으로 채우지 않고 거절하며, 두 통로에 서로 다른 값을 담아도 거절합니다. 거절은 HTTP 422 이고 오류 코드는 VALIDATION_ENV_SCOPE_MISMATCH 입니다.

## Headers

- `Idempotency-Key` string, nullable
- `Authorization` string, nullable
- `X-Env-Scope` 'production' | 'real_test' | 'sandbox', nullable: 환경을 고릅니다. 값은 production · real_test · sandbox 중 하나입니다. 경로 접두(/v1/sandbox/... · /v1/test/... · /v1/production/...) · X-Env-Scope 헤더 · env_scope 쿼리 가운데 한 통로로 반드시 보내야 합니다. 빼면 자동으로 채우지 않고 거절하며, 두 통로에 서로 다른 값을 담아도 거절합니다. 거절은 HTTP 422 이고 오류 코드는 VALIDATION_ENV_SCOPE_MISMATCH 입니다.

## Request body

- ProductCreateRequest: `POST /v1/products` 요청 본문. SCHEMA → DTO 매핑: - `code`/`name`/`description`/`category`/`status`/`visibility`/`organization_id` → 직접 매핑. `env_scope`는 헤더 `X-Env-Scope`에서 주입(본문에 없음). - `metadata` JSONB → 자유 형식. 정책 키 진입 차단은 DB CHECK 1차 + 서비스 1차. - `initial_version` → 동봉 시 단일 트랜잭션 안 자식 INSERT. immutable 컬럼: `code`/`organization_id`/`env_scope`/`current_version_id` 는 본 요청 DTO 외 PATCH 요청에 포함되면 `extra='forbid'`로 422 차단.
  - `code` string, required: 비즈 식별 코드. 환경 내 + 발행자 범위 내 유일해야 한다. 중복이면 409 CATALOG_DUPLICATE_CODE 로 거절한다.
  - `name` string, required: 사용자 노출 명칭. 공백만 입력은 거절한다.
  - `description` string, nullable: 카탈로그 설명(마크다운 허용 — UI 렌더링 시 HTML sanitization 의무, raw HTML/javascript: scheme 차단 = UI 트랙 책임). SCHEMA `description TEXT NULL` 최대 10000자.
  - `category` 'tax' | 'gov' | 'finance' | 'marketing', nullable: 카탈로그 그룹화 enum (`tax`/`gov`/`finance`). 비워 둘 수 있다.
  - `status` 'DRAFT' | 'ACTIVE' | 'DEPRECATED' | 'RETIRED': 초기 상태. 기본 DRAFT. `DRAFT`/`ACTIVE`만 발행 가능 — `DEPRECATED`/`RETIRED` 직접 생성 금지(`trg_api_products_insert_must_be_draft` 트리거가 DRAFT 외 차단). DTO 레벨에서는 enum만 잠그고 강제는 트리거/서비스에서.
  - `visibility` 'PUBLIC' | 'PRIVATE' | 'ENTERPRISE': 가시성 enum. 기본 PRIVATE.
  - `organization_id` integer, nullable: NULL=글로벌 카탈로그(v1 운영 정책 권장). NOT NULL=조직 전용 사설 상품 (v1 운영 정책으로 차단 — 403 POLICY_TIER_FORBIDDEN. 별도 결정 뒤 해제). INT4 범위 잠금.
  - `monitoring_grade` integer, nullable: Ops monitoring grade 1..5 (api_products.monitoring_grade).
  - `website_slug` string, nullable: Public Website slug for /service-products routing. Must align with page_content.content.slug when both set. 소문자 · 숫자 · 사이 하이픈만.
  - `collection_sites` string[], nullable: Collection institution/vendor codes (product_collection_site SSOT). When set, replaces active site rows for the product env_scope.
  - `metadata` object, nullable: 자유 메타데이터. 정책 키(`status`/`visibility`/`provider`/`action`/`env_scope`/`organization_id`/`plan_id`/`code`) 진입 금지 — DB CHECK 1차 + 서비스 1차. JSON 직렬화 후 최대 65536 bytes.
  - `initial_version` InitialVersionRequest: `POST /v1/products` 요청 본문에 nested로 동봉되는 초기 버전. 초기 버전 동시 발행 옵션이다. 동봉 시 단일 트랜잭션으로 `api_products` + `api_product_versions` 동시 INSERT.
    - `version` string, required: 버전 식별자. 같은 상품 안에서 유일해야 한다. 형식 `^[A-Za-z0-9._-]{1,20}$`.
    - `provider` string, required: 등록된 기관 식별자. 등록되지 않은 값이면 400 으로 거절한다. 형식 `^[a-z][a-z0-9_]{0,63}$`.
    - `action` string, required: 등록된 액션 식별자. 기관에 종속한다. 형식 `^[a-z][a-z0-9_.]{0,63}$`.
    - `params_schema_ref` string, nullable: `provider.action.vN` 형식. 작업 결과의 schema 값과 같은 형식이다.
    - `result_schema_ref` string, nullable: 동상.
    - `pricing_hint` object, nullable: 가격 표시 힌트. 정책을 결정하는 키는 넣을 수 없다. JSON 직렬화 후 최대 65536 bytes.
    - `rate_limit_hint` object, nullable: rate limit 표시 힌트. 정책을 결정하는 키는 넣을 수 없다. JSON 직렬화 후 최대 65536 bytes.
    - `metadata` object, nullable: 자유 메타데이터. 정책을 결정하는 키는 넣을 수 없다. JSON 직렬화 후 최대 65536 bytes.
    - `publish_immediately` boolean: true 이면 발행 트랜잭션 안에서 version_status='PUBLISHED' + api_products.current_version_id 동시 채움. false 면 DRAFT 상태로 발행.

## Response `201`

Successful Response

- ProductResponse: 상품 단일 객체 응답. SCHEMA → DTO 매핑: - `api_products.id` → `id`. - `code`/`name`/`description`/`category`/`status`/`visibility`/`organization_id`/ `env_scope`/`current_version_id`/`metadata`/`created_at`/`updated_at`/`deleted_at` → 직접 매핑. - `current_version` (nested) — `GET /v1/products/{id}` 상세 응답에서만 채운다. `POST /v1/products` 응답은 `initial_version` echo만 동봉한다. - `versions` (List) — `GET /v1/products/{id}?include_versions=true` 시만 채운다. lifecycle timestamp 3종(`activated_at`/`deprecated_at`/`retired_at`)은 본 응답에 미노출 (운영 추적성은 운영 API 영역). UI/콘솔이 필요하면 별도 결정 뒤 노출을 넓힌다.
  - `id` integer, required: api_products.id.
  - `code` string, required
  - `name` string, required
  - `description` string, nullable: 카탈로그 설명. `ProductCreateRequest.description` 과 같은 정책이다 - 화면이 sanitize 를 맡고 최대 10000자.
  - `status` 'DRAFT' | 'ACTIVE' | 'DEPRECATED' | 'RETIRED', required
  - `visibility` 'PUBLIC' | 'PRIVATE' | 'ENTERPRISE', required
  - `category` 'tax' | 'gov' | 'finance' | 'marketing', nullable
  - `organization_id` integer, nullable
  - `env_scope` 'sandbox' | 'real_test' | 'production', required
  - `current_version_id` integer, nullable: 활성 버전 포인터. DRAFT 상태에서만 NULL 허용 — ACTIVE/DEPRECATED/RETIRED 상태에서는 반드시 있어야 한다.
  - `metadata` object: 자유 메타데이터. 정책을 결정하는 키는 넣을 수 없다. JSON 직렬화 후 최대 65536 bytes.
  - `monitoring_grade` integer, nullable: Ops monitoring grade 1..5.
  - `website_slug` string, nullable: Public Website slug for routing.
  - `collection_sites` string[]: Active collection site codes (product_collection_site SSOT).
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `deleted_at` string, date-time, nullable: soft delete 시각. NULL=활성, NOT NULL=삭제 완료.
  - `current_version` CurrentVersionSummary: 상품 응답에 nested 로 동봉되는 활성 버전 요약. `GET /v1/products`(목록) + `GET /v1/products/{id}`(상세) 응답에 동봉. 전체 `ProductVersionResponse`(11+ 필드)가 아닌 운영자 카탈로그 표시에 필요한 핵심 필드만 추려 응답 크기를 줄인다. version · provider · action 셋은 형식과 길이 64자를 상품 버전 쪽과 같은 선언으로 잠근다 - 두 자리가 갈리면 한쪽만 고쳐진다.
    - `id` integer, required: api_product_versions.id.
    - `version` string, required: 버전 식별자(예: 'v1'/'v2'). 형식 `^[A-Za-z0-9._-]{1,20}$`.
    - `version_status` 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' | 'RETIRED', required: 버전 상태 enum.
    - `provider` string, required: 등록된 기관 식별자. 형식 `^[a-z][a-z0-9_]{0,63}$`.
    - `action` string, required: 등록된 액션 식별자. 기관에 종속한다. 형식 `^[a-z][a-z0-9_.]{0,63}$`.
  - `mcp_tool_name` string, nullable: 이 상품의 활성 버전 (provider, action) 에 해당하는 MCP 도구 이름. null 이면 그 쌍에 도구 행이 없다(활성 버전이 없을 때도 null).
  - `mcp_tool_enabled` boolean: 그 도구가 켜져 있고 이 env_scope 에서 팔리는가. false 는 꺼졌거나 이 레인에서 팔지 않는다는 뜻이며 '준비 중' 을 뜻하지 않는다.
  - `facts` ProductFacts: 상품 상세페이지 안내 블록이 읽는 **서버가 아는 사실**. 도구 계약에서 만들어 손으로 적지 않는다. 모르는 값은 null 로 둔다 - 그럴듯한 값으로 채우지 않는다. 특히 `irreversible` 은 조회면 false 이고 쓰기는 상품이 되돌릴 수 없음을 선언하기 전까지 null 이다(쓰기가 「기관 상태를 바꿀 수 있음」 이라는 것과 「되돌릴 수 없음」 은 다른 말이다).
    - `credential` ProductFactsCredential, required: 이 상품을 쓰기 전에 **연동해 둬야 하는 자격**.
      - `required` boolean, required: 연동해 둔 자격이 필요한가. false 면 연동 없이 바로 부를 수 있다.
      - `kind` 'certificate' | 'id_or_key' | 'bank_account', nullable, required: 자격의 종류: `certificate`(공동인증서), `bank_account`(은행 계좌 연동), `id_or_key`(아이디 · 키 · 계정 연결). 연동이 필요 없으면 null. 여러 기관이 섞이면 인증서 > 은행 계좌 > 아이디 · 키 순으로 하나만 말한다.
      - `windows_pc_needed` boolean, required: 인증서 연동에 Windows PC 와 연결 프로그램 설치가 한 번 필요한가.
      - `providers` string[], required: 연동해야 하는 기관의 슬러그 목록(중복 없음, 호출 순서). 연동이 필요 없으면 빈 목록.
    - `prerequisite_login` ProductFactsPrerequisiteLogin, required: 이 상품을 부르기 전에 먼저 불러야 하는 로그인.
      - `provider` string, required: 로그인해야 하는 기관의 슬러그.
      - `provider_label_ko` string, required: 그 기관의 한국어 이름(서버 표에서 유도한 값).
    - `effect` 'read' | 'draft' | 'submit', nullable, required: 기관에 하는 일: `read`(조회만), `draft`(초안을 덧붙일 뿐 기존 상태를 바꾸지 않음), `submit`(기관 상태를 바꿀 수 있음 - 접수 · 발급 · 저장 등). 로그인 · 세션 · 입력 서식 초기화 같은 단계처럼 분류하지 않은 도구는 null.
    - `irreversible` boolean, nullable, required: 되돌릴 수 없는가. 조회는 false. 쓰기는 null(알 수 없음) - `effect` 가 `submit` 이라는 것이 되돌릴 수 없다는 뜻이 아니다.
    - `confirm_required` boolean, required: 쓰기가 먼저 확인(미리보기 · 동의)을 거쳐야 끝나는가. 조회는 false.
    - `limits` ProductFactsLimits, required: 결과 파일에 관한 한도. 파일을 내주지 않는 상품은 둘 다 null.
      - `file_link_minutes` integer, nullable: 결과 파일 내려받기 링크가 열려 있는 분.
      - `file_retention_days` integer, nullable: 결과 파일을 보관하는 일수.
  - `initial_version` InitialVersionEcho: `POST /v1/products` 응답에 nested로 동봉되는 초기 버전 요약. 초기 버전을 동봉했을 때 응답 본문의 `initial_version` nested 가 이 모양이다. `version` · `provider` · `action` 은 상품 등록과 같은 꼴 · 같은 길이(최대 64자)로 잠근다.
    - `id` integer, required: api_product_versions.id.
    - `version` string, required
    - `version_status` 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' | 'RETIRED', required
    - `provider` string, required
    - `action` string, required
    - `published_at` string, date-time, nullable: 발행 시각. publish_immediately=false 면 null.
  - `versions` CurrentVersionSummary[], nullable: GET /v1/products/{id}?include_versions=true 시만 채운다. 그 외 응답은 항상 null.
    - `id` integer, required: api_product_versions.id.
    - `version` string, required: 버전 식별자(예: 'v1'/'v2'). 형식 `^[A-Za-z0-9._-]{1,20}$`.
    - `version_status` 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' | 'RETIRED', required: 버전 상태 enum.
    - `provider` string, required: 등록된 기관 식별자. 형식 `^[a-z][a-z0-9_]{0,63}$`.
    - `action` string, required: 등록된 액션 식별자. 기관에 종속한다. 형식 `^[a-z][a-z0-9_.]{0,63}$`.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/xdata/apis/xboss-api.md) · [All operations](https://skmtc.dev/xdata/apis/xboss-api/llms.txt) · [OpenAPI document](https://skmtc.dev/xdata/apis/xboss-api/revisions/816de78950db?raw)
