Imported from asungtrading/asung-wms (
.claude/skills/cin7-api/SKILL.md). Install upstream withnpx skills add asungtrading/asung-wms --skill cin7-api. Copyright stays with the author.
Cin7 Core API v2 연동 스킬
기본 정보
- Base URL:
https://inventory.dearsystems.com/ExternalApi/v2/ - 인증 헤더 (두 개 모두 필수):
api-auth-accountid: Account IDapi-auth-applicationkey: API Application Key
- 데이터 포맷: JSON only
- 날짜 포맷: ISO 8601 (
2024-01-15T00:00:00또는2024-01-15)
Apps Script 인증 패턴 (Asung 표준)
⚠️⚠️ Script Property 이름은 CIN7_APPLICATION_KEY 다 — 2026-08-05 정정.
이 문서는 CIN7_API_KEY 로 적혀 있었고, asung-apps-script 스킬은 CIN7_APPLICATION_KEY 로
적혀 있었다. 두 스킬이 갈라져 있어 2026-08-05 실호출이 한 번 실패했다(폴백으로 진행).
CIN7_APPLICATION_KEY 로 통일한 근거는 문서 4 : 1:
asung-apps-script/SKILL.md 규칙 1 표준 키 목록 · shopify-tracking/references/customer-master.md
(⚠️ "CIN7_API_KEY 아님" 이라고 명시) · asung-wms/SKILL.md 환경 상수 ·
asung-wms/references/edge-function.md(Supabase secret 이 같은 이름으로 실동작 중) 대(對)
이 문서 한 곳. ⚠️ 단 GAS Script Properties 실물은 이번에 열어보지 않았다 — 헤더 이름
(api-auth-applicationkey)과 Script Property 키 이름은 별개이고, 확증은 Script Properties 화면뿐이다.
호출이 401/403 이면 이름을 의심하고 Script Properties 를 먼저 열어 확인할 것(getProp 은
없는 키에 Missing Script Property: … 를 throw 하므로 증상이 분명하다).
// Config.gs의 getProp() 사용 — API Key를 코드에 직접 쓰지 말 것
const BASE_URL = 'https://inventory.dearsystems.com/ExternalApi/v2/';
function getCin7Headers() {
return {
'api-auth-accountid': getProp('CIN7_ACCOUNT_ID'),
// ⚠️ CIN7_API_KEY 가 아니다 (위 정정 참조)
'api-auth-applicationkey': getProp('CIN7_APPLICATION_KEY'),
'Content-Type': 'application/json'
};
}
function cin7Get(endpoint, params) {
const url = BASE_URL + endpoint + '?' + Object.entries(params)
.filter(([_, v]) => v !== null && v !== undefined)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
const response = UrlFetchApp.fetch(url, {
method: 'GET',
headers: getCin7Headers(),
muteHttpExceptions: true
});
if (response.getResponseCode() !== 200) {
throw new Error(`Cin7 API Error ${response.getResponseCode()}: ${response.getContentText()}`);
}
return JSON.parse(response.getContentText());
}
페이지네이션 패턴
페이지네이션을 지원하는 엔드포인트: saleList, purchaseList, stockadjustmentList, stockTakeList, stockTransferList, product, ref/productavailability
function fetchAllPages(endpoint, params) {
const allItems = [];
let page = 1;
while (true) {
const data = cin7Get(endpoint, { ...params, Page: page, Limit: 100 });
// 엔드포인트별 배열 키 이름이 다름 — 아래 엔드포인트 섹션 참고
const items = data.SaleList || data.PurchaseList || data.StockAdjustmentList ||
data.StockTransferList || data.ProductAvailabilityList || data.Product || [];
allItems.push(...items);
if (items.length < 100 || allItems.length >= data.Total) break;
page++;
Utilities.sleep(1200); // Rate limit — 분당 50콜(한도 60/60의 여유분, 주의사항 3·11번)
}
return allItems;
}
엔드포인트 레퍼런스
상세 파라미터와 응답 구조는 /references/ 폴더의 각 파일을 참고하세요:
| 목적 | 엔드포인트 | 레퍼런스 파일 |
|---|---|---|
| 판매 목록 / 상세 | GET /saleList, GET /sale |
references/sale.md |
| 발주 목록 / 상세 | GET /purchaseList, GET /purchase |
references/purchase.md |
| DRAFT 발주 생성 (쓰기) | POST /purchase → POST /purchase/order (2단계 필수) |
references/purchase-write.md |
| 공급사 단가(Fixed Price) 갱신 (쓰기) | PUT /product-suppliers (읽기는 GET /product?IncludeSuppliers=true) |
references/product-suppliers-write.md |
| 트랜스퍼/입고 쓰기 (bin GUID 필수) | POST /stockTransfer, POST /purchase/stock, PUT /stockTransfer(완료 — ⚠️수량 변경 무시) |
references/stock-write.md |
| bin GUID 조회 | GET /ref/location → 창고 행 Bins[] |
references/stock-write.md 5절 |
| Reference Book 여섯 (마스터 · 전량 실측 2026-09-11) | GET /ref/brand·/ref/category·/ref/unit·/ref/paymentterm·/ref/account(⚠️키 AccountsList)·/ref/location — ⚠️ 통화 목록 없음 |
references/ref-endpoints.md |
| 고객 목록 / 상세 | GET /customer |
references/customer.md |
| 공급업체 목록 / 상세 | GET /supplier |
references/supplier.md |
| 제품 마스터 | GET /product |
references/product-master.md |
| 재고 현황 | GET /ref/productavailability |
references/product.md |
| 재고 조정 | GET /stockadjustmentList, GET /stockadjustment |
references/stock.md |
| 브랜치 이전 | GET /stockTransferList, GET /stockTransfer |
references/stock.md |
| 회계 트랜잭션 | GET /transactions |
references/transactions.md |
| 전체 엔드포인트 목록 | 모든 v2 엔드포인트 인덱스 + apib 라인 번호 | references/endpoint-index.md |
자주 쓰는 Use Case 패턴
1. 고객별 구매 중단 상품 감지 (UpdatedSince 활용)
// 최근 N일간 판매 없는 고객-SKU 조합 찾기
const sales = fetchAllPages('saleList', {
UpdatedSince: new Date(Date.now() - 90 * 86400000).toISOString(),
CombinedInvoiceStatus: 'AUTHORISED'
});
// 각 SaleID로 /sale 상세 호출하여 라인 아이템 추출
2. 재고 현황 전체 조회
const availability = fetchAllPages('ref/productavailability', {
Location: 'Toronto Warehouse' // 또는 빈 문자열로 전체
});
// 응답: ProductAvailabilityList[].{ SKU, OnHand, Available, OnOrder, InTransit }
3. 브랜치 트랜스퍼 완료 건 조회
const transfers = fetchAllPages('stockTransferList', {
Status: 'COMPLETED',
Search: ''
});
// 각 TaskID로 /stockTransfer?TaskID={} 호출하여 라인 상세 조회
4. 재고 조정 이력 조회
const adjustments = fetchAllPages('stockadjustmentList', {
Status: 'COMPLETED'
});
// 각 TaskID로 /stockadjustment?TaskID={} 호출하여 조정 라인 조회
주의사항
- 쓰기(POST) 작업: Purchase/Sale 등 데이터를 생성·수정하는 코드는 반드시
references/purchase-write.md를, 재고 이동/입고(트랜스퍼·stock received)는references/stock-write.md를(bin은 GUID만·Invoice First 게이트·Date 필수 등 실측 확정) 먼저 읽을 것. 공식 문서와 실제 동작이 다른 부분(2단계 생성, 환율 자동 물림)이 실측으로 정리되어 있음. 새 write 코드는 항상 DRY_RUN 게이트를 거치고, Status는 DRAFT만 사용 (Authorize는 사람 몫).- ⚠️⚠️ 쓰기 검증은 HTTP 200 이 아니라 GET 으로 되읽은 값으로 한다 (2026-07-28 원칙). Cin7 은 요청을 200 으로 받고 조용히 무시하는 경우가 있다 — 창고간 트랜스퍼 완료 PUT 의
TransferQuantity변경이 그렇다(TR-03267: 요청 4 → 저장 2, 요청 2 → 저장 4, 양방향 무시). 이 때문에 "수량 초과 완료 허용"이라는 틀린 기록이 한동안 남아 있었다 — 정정 내용은references/stock-write.md2절.
- ⚠️⚠️ 쓰기 검증은 HTTP 200 이 아니라 GET 으로 되읽은 값으로 한다 (2026-07-28 원칙). Cin7 은 요청을 200 으로 받고 조용히 무시하는 경우가 있다 — 창고간 트랜스퍼 완료 PUT 의
- List vs Detail 패턴:
*List엔드포인트는 요약 정보만 반환. 라인 아이템이 필요하면 TaskID/SaleID로 상세 엔드포인트를 별도 호출해야 함. - 응답 배열 키 이름: 엔드포인트마다 다름.
SaleList,PurchaseList,StockAdjustmentList,StockTransferList,ProductAvailabilityList,CustomerList등. - Rate Limit = 60콜/60초 · 애플리케이션 키 단위 (2026-08-18 밤 실측 확정 — 상세는 아래 11번): 루프에서
Utilities.sleep(1200)권장(분당 50콜 — 같은 키의 다른 주체와 겹칠 여유.종전 300ms는 분당 200콜로 한도의 3.3배였다). ⚠️ sleep 만으로는 부족하다 — 같은 키를 쓰는 다른 호출과 겹치면 429 가 그래도 온다. 키가 WMS 용과 GAS 용으로 분리돼 있고 한도는 키마다 독립이라 GAS 프로브는 WMS 키 예산과 무관하다(계정 단위 아님 — 11번의 키 범위 실측). - 날짜 필터:
UpdatedSince,CreatedSince등은 UTC 기준 ISO 8601. - Simple vs Advanced: Sale/Purchase 모두 Simple/Advanced 타입 존재. Advanced는 여러 Invoice/Fulfilment 가질 수 있음.
- bin GUID 는
/ref/location최상위 창고 행의Bins[]에서 — 응답은 Total 2678 에Limit 500으로 잘리지만Bins[]는 창고 행 하나에 전부 들어있다(에드먼튼 628 · 토론토 2047).⚠️ child-location 행의→ ⚠️⚠️ [정정 2026-09-11] 틀린 기록이었다.Name은 bin 이름이 아니다(바코드류).ref/location전량 2,678행(GAS 프로브 ·Limit페이지네이션으로 전부 수집)에서 하위 행(ParentID있음) 2,675개의Name이 창고Bins[]의Name과 2,675/2,675 일치 · 숫자만인 이름 0 · 20자 이상 0 — 바코드는 섞여 있지 않다.Bins[]원소{ID, Name, IsDeprecated, IsStaging}의ID= 하위 행의ID(같은 bin GUID) — 어느 쪽에서 얻어도 같다. ⚠️ 원래 기록("071164313169"같은 바코드류)이 무엇을 본 것인지는 구간·기준 불명 — 추측하지 않는다.Bins[]경로가 잘림 없이 안전한 것은 여전히 사실이다.references/stock-write.md5절 ·references/ref-endpoints.md. purchaseList필터는 직관과 다르다 —InvoiceStatus·Status모두 단일 값만(콤마·파이프로 여러 값 = Total 0 → 상태별 개별 호출 +IDdedup) ·Limit=1000동작 · 기본 정렬이 PO 번호 오름차순이라 최신 PO 가 마지막 페이지(잘리면 항상 최신부터 누락) ·UpdatedSince는 최신성 보장 못함 ·Type은 무시됨 ·StockReceivedStatus는 동작한다(⚠️ 아래 12번 — 종전 "무시됨" 기록은 파라미터 이름 오타였다). 좁힐 땐InvoiceStatus가 아니라Status로 — 실측 973행→78행. 실측 표는references/purchase.md.saleList는OrderStatus(승인 상태)와Status(진행 상태)가 독립된 축이다 —OrderStatus=AUTHORISED인 오더의Status는ORDERED로 남는 게 정상(Simple·Advanced 공통). Advanced Sale 도 라인 구조가 같아Type필터는 불필요.references/sale.md.
- 화면으로만 되는 작업(재고 재평가·bin 재고 리포트)은
references/stock.md하단 「Cin7 UI 실측 노트」. ⚠️ 원가 0 재고 재평가는 방법 미확정(Non-zero 0 + Zero stock 재입력은 상계되지 않고 재고가 2배가 된다). - ⚠️⚠️ 트랜스퍼 Put away 는 v2 API 에 없다 (2026-07-31 TR-03259 실측 — 탐색 종결). UI 의 라인별 Put away/LOCATION 은 API 로 노출되지 않는다(
/stockTransfer/putaway등 전부 404, 본 문서에PutAway플래그도 없음, CSV Import 도 없음) → 헤더To착지 + bin 별 별도 트랜스퍼가 유일한 경로. 같은 탐색을 반복하지 말 것.references/stock-write.md「Put away」절. - 쓰기 되읽기 검증·bin 단위 재고 확인은
/ref/productavailability— 판정은 OnHand(Available 은 판매 배정 차감이라 오판) · SKU/창고/Bin 정확 일치 ·Total > 반환 행수면 미확인 처리.references/stock-write.md. - ⚠️⚠️ Rate limit 은 실측으로 확정됐다 (2026-08-18 밤 GAS 프로브) — 60콜/60초 · 애플리케이션 키 단위 · 사전 제어 불가. 추측하지 말 것 — 429 본문 명문:
You have reached 60 calls per 60 seconds API limit.- 범위 = 애플리케이션 키 단위 — GAS 키가
remaining=0(429)인 그 순간 WMS 키로 HTTP 200. ⚠️ 두 키의AccountID는 동일 — 계정이 같아도 한도는 키마다 독립된 60콜. 다만 같은 키를 쓰는 주체들끼리는 공유다 — WMS 키는hello(5분 폴링)·receiving·inv-collect가 함께 쓴다(⚠️ 코드 확인 미완 — 사실 확인 필요). ⇒ 용도별 키 분리는 한도를 늘리는 실질적 수단이다. 단 ⚠️ Cin7 약관상 허용 여부는 미확인이고, 키가 늘면 교체·분실 관리 지점도 함께 는다. - ⚠️⚠️ 200 응답에는
x-ratelimit-*헤더가 오지 않는다(실측 — 헤더 전문 덤프에Cache-Control·Content-Type·Set-Cookie·Date등만). 그 헤더들(x-ratelimit-limit: 60·x-ratelimit-remaining·x-ratelimit-reset·Retry-After: 60 Seconds)은 429 응답에만 온다. 그래서 "남은 콜 수를 보고 미리 쉬는" 사전 제어는 불가능 — 429 를 맞고 나서만 알 수 있다. ⇒ 회차당 호출 수를 미리 묶는 캡이 유일한 예방책이다(MAX_DETAIL_PER_SOURCE가 존재해야 하는 이유 — 종전에는 근거가 기록돼 있지 않았다). - ⚠️⚠️ 회복은 실측 31초였지만(429 후 5초 간격 재시도, 6회째 +31s 에 200)
Retry-After: 60 Seconds표기값을 신뢰해 기다릴 것 — 31초는 단발 관측이고, 서버가 60을 말하면 60을 지키는 것이 계약이다. ⚠️ 종전 백오프(1.5s→3s · 상한 2회 = 최대 4.5초)는 실측 회복 시간의 1/7 로 사실상 무의미하다 — 재시도는 반드시Retry-After헤더 값을 읽어 그만큼 쉬어야 한다(없으면 60초 가정). ⚠️_shared/cin7.ts·inv-collect의 현행 상수는 아직 옛 값 — 수정은 원장 ⑤ 작업 항목. - 페이싱 계산식: 간격(ms) = 60,000 / 목표 분당 콜수. 1,000ms = 정확히 한도 · 1,200ms = 분당 50콜(권장 — 같은 키를 쓰는 다른 주체와 겹칠 여유). 차단 지점 실측: 무간격 연타 53콜째(18초)/61콜째 — 앞선 호출이 같은 60초 창에 남아 있으면 더 빨리 걸린다.
- 429 라도 페이지 순회에서 즉시 throw 하지 말 것 — 1페이지 성공 후 2페이지 429 에 throw 하면 회차 전체가 죽고 조용한 부분 스캔이 남는다(2026-08-04 SO-14100/14106 미유입 실사고). 패턴:
Retry-After만큼 대기 후 재시도 → 소진 시 throw 없이 회차 조기 종료 +rate_limited/rate_limited_at_page진단 필드 노출. 429 외 4xx/5xx 는 throw 유지. Asung WMS 는 이 HTTP 레이어를supabase/functions/_shared/cin7.ts로 공용화해hello·receiving두 Edge Function 이 함께 쓴다 — ⚠️ 그 파일을 고치면 두 함수 모두 재배포(각 함수는 배포 시점 번들을 쓴다).
- 범위 = 애플리케이션 키 단위 — GAS 키가
- ⚠️⚠️ 파라미터가 무시되는 것처럼 보이면 이름 오타를 먼저 의심하라 (2026-08-04 교훈). Cin7 은 모르는 파라미터를 조용히 무시하므로 응답만으로 "오타" 와 "미지원" 이 구별되지 않는다 — 둘 다 무필터와 같은 Total 이다. 실제로
RestockReceivedStatus(apib 표기, 존재하지 않는 이름)를 보내고 "서버 필터 불가" 라고 잘못 기록해 일주일간 전량 조회 + 클라이언트 필터를 짊어졌다(정답은StockReceivedStatus: 585 vs 877). 검증 순서 = ①references/스펠링 대조(⚠️ apib 파라미터 표기 ≠ 응답 필드명일 수 있다) ②동작을 아는 파라미터를 같이 보내 배선 확인 ③그래도 Total 불변이면 "미지원" 기록. 200 은 파라미터를 받아들였다는 뜻이 아니다(0번의 "검증은 되읽기로" 와 같은 계열). - ⚠️⚠️ Advanced Purchase 는 주소·파라미터·응답 구조가 모두 다르다 — 그리고 쓰기 폴백은 Simple→Advanced 한 방향만 (2026-08-07). 세 번 밟은 함정의 규칙 승격: ①
GET /purchase/invoice가 Advanced 에서 400 deprecated(2026-08-05) ②POST /purchase/stock도 Advanced 에서 400 deprecated — PO-01094 Apply 8개 bin 그룹 전멸(2026-08-07 실측:"This endpoint is deprecated and does not support Advanced Purchase and Service Purchase") ③purchaseList의Type서버 필터 무시. Advanced 전용 주소 =/advanced-purchase/stock·/advanced-purchase/invoice·/advanced-purchase/put-away. 식별자 =TaskID(=PO GUID) 하나가 아니라PurchaseID(PO GUID, 필수) +TaskID(하위 태스크 GUID — 생략 시 새 태스크 생성). 응답 = 단일 문서가 아니라 태스크 배열(StockReceiving/Invoice[]/PutAway— 인보이스 블록 객체↔배열과 같은 패턴). ⚠️⚠️ 역방향 폴백 절대 금지 — apib 명문: "If POST or PUT methods called for Simple Purchase, this purchase will be converted to Advanced Purchase." Simple PO 에 advanced 주소로 쓰면 에러가 아니라 PO 가 조용히 Advanced 로 변환된다 — 읽기 폴백(400 이면 반대쪽 1회 재시도)을 쓰기에 양방향으로 이식하면 타입 기록이 틀린 Simple PO 가 하나씩 오염된다. 쓰기 폴백은/purchase/stock400 deprecated → advanced 재시도 한 방향만(그 400 은 "이 PO 는 Advanced" 의 확정 신호라 안전 — 반대 방향에는 확정 신호가 존재하지 않는다). ⚠️⚠️ 2026-08-07 프로브 실측 2건 추가(상세references/stock-write.mdAdvanced 절): ① Advanced stock received 에는 bin 을 못 싣는다 —Lines[].LocationID는 200 후 되읽으면 null(선반 지정은 별도/advanced-purchase/put-away, 2단 구조). ⚠️ 읽기에서도 같은 구조가 확인됐다(2026-08-18 — 원장 수집 실측):GET /advanced-purchase?ID=의 SR 라인LocationID는 null 이거나 창고 GUID 이고, bin 은PutAway블록에만 있다 — 쓰기·읽기 양쪽에서 독립적으로 확인된 사실(교차 검증) ②DELETE /advanced-purchase/stock은 200 을 주지만 태스크를 지우지 않는다(R11 계열 — 200≠반영. API 로 입고 태스크 제거는 미확인, 지울 수 있다고 가정하고 설계하지 말 것). ⚠️⚠️ 그리고TaskID는 I&R 그룹 식별자다(2026-08-07 실사고 2호) — stock/put-away POST 에 지정하지 않으면 새 그룹이 생기고 빈 DRAFT 인보이스가 딸려 만들어진다(승인 인보이스와 입고가 영구히 갈라짐 — PO-01094). 반드시 승인된 인보이스의 TaskID 를 지정할 것(정확히 1개일 때만 진행 · 외부 그룹에 라인 있으면 중단 —stock-write.md11번). 프로브 도구는 asung-wms repodocs/probes/WmsAdvPoStockProbe.gs. apib 원문이 로컬에 없으면curl -sL https://jsapi.apiary.io/apis/dearinventory.apib(공개 문서 — Advanced stock 16918행·put-away 17347행). - ⚠️⚠️ Simple 발주는 인보이스 수량과 입고 수량의 완전 일치를 요구한다 (2026-08-18 실측). 불일치 시 승인이 400 으로 거부된다:
"Product XXX has quantity invoiced 192.0000 which is different from quantity received 168.0000. Quantity received should exactly match the quantity invoiced."UI 에서도 같은 에러가 나므로 수동 승인으로 우회할 수 없다. 해소 경로 =Convert(Simple→Advanced). Advanced 는 Invoice / Stock received / Put away 가 독립 태스크라 인보이스 192 · 입고 168 이 공존할 수 있고, Convert 하나로 승인까지 완결된다(별도 Authorize 클릭 불필요 — 실측 PO-01117). 📌 초과 입고는 클램프로 맞출 수 있지만 미달은 구조적으로 못 맞춘다 — 없는 물건을 있다고 쓸 수 없기 때문. 그래서 WMS 의auto-authorize는 미달 입고에서 반드시 실패한다. ⚠️ 따라서 WMSapply_note의WARN auto-authorize failed는 사고가 아니다 — Simple 구조상 예정된 거부이고, Convert 로 해소된다. [실측] 2026-07-24 이후 10건 전부 이 계열이었고 전부 정상 처리됐다(9건PA=AUTHORISED확인). ⚠️ 경고문이 실제보다 심각하게 읽히는 것이 문제다 — 진짜 실패와 구별이 안 된다(별건: 문구 개선 · 화면 표시). - ⚠️
IsService와Type=Non-inventory는 다른 개념이다 (2026-08-24 — FINAL-SALE 실사고).IsService= 서비스(팔되 실물이 없음 — 운임·수수료류) / productType=Non-inventory= 재고 추적을 안 하는 품목(팔아도 Cin7 이 재고를 안 움직인다). 둘은 독립 축이라IsService=false 인데 Non-inventory가 실재한다 — [실물]FINAL-SALE(파손품을 adjustment 로 이미 뺀 뒤 판매할 때 쓰는 껍데기 SKU · MARGIN 100% · Comment 에 실제 상품 코드). IsService 필터만 있으면 이 틈으로 빠진다 — 원장 수집이 이 SKU 의 판매를 재고 사건으로 잡아 -34 를 쌓았다(⑥ 첫 대조가 발견). ⚠️ 재고 판별이 필요하면 productType으로 — 단purchaseList의Type서버 필터는 무시되고(7번), 문서 라인 레벨에는NonInventory가 SR 라인에만 있다(13번 — PA·판매 Pick 라인은 미실측/부재). SKU 목록 확보는/product전량의Type집계가 확실하다. - ⚠️⚠️
productList(/product)는Limit을 명시해야 한다 — 기본 100건이다 (2026-08-24 실측).Limit없이Page만 올리면 매 페이지가 같은 100건이라 페이지를 아무리 돌려도 전량이 안 온다.Limit=1000+ 페이지네이션으로 5,001건 전량 수신 확인. 📌 처음에 「응답 배열 키 이름 문제」로 오진했다 — 키를 동적으로 찾아내도록 고쳐도 0행이라 그제야Limit이 원인임을 알았다. ⚠️ 계열 함정: 파라미터가 틀려도 200 이 온다(6번 「조용히 무시」·이미지IncludeAttachments). 행 수가 이상하면 파라미터부터 의심할 것 — 에러는 안 난다. - ⚠️ product
Type의 실제 값은Non Inventory(공백) 다 —Non-inventory(하이픈)가 아니다 (2026-08-24 실측 · 화면 표기와 다르다).product_type = 'Non-inventory'로 SQL·코드 필터를 쓰면 조용히 0행이 된다. 📌 원장의 비재고 게이트가Type !== 'Stock'부정 조건을 쓰는 이유가 이것이다 — 값 어휘를 맞히지 않아도 되고, 새 타입이 생겨도 자동으로 차단 쪽에 선다. - ⚠️⚠️ Cin7 은 없는 경로에 404 가 아니라 200 + HTML 을 돌려준다 (2026-09-13 실측).
product/channels·productChannel·saleChannel·ref/saleChannel·ref/channel·externalService·ref/externalService일곱 모두 HTTP 200 에 「Page not found」 HTML. ⇒ HTTP 코드로 엔드포인트 존재를 판정하지 마라 — 본문이 JSON 인지 HTML 인지를 봐라. (12번 「200 은 파라미터를 받아들였다는 뜻이 아니다」와 같은 뿌리.) 📌 제품 Channels 탭은 API 에 없다 — 화면 전용. - ⚠️ 배열 키 이름에 규칙이 없다.
ProductFamilies(List 접미사 없음) ·AccountsList(혼자 복수형) ·BrandList·LocationList·Products. ⇒ 키를 이름으로 짐작하지 말고 「어느 값이 배열인가」로 찾아라. 📌GET /product에IncludeBOM=true를 켜면Limit=500이 실효 상한이다 (1000 을 보내도 안 온다 · 18,829 = 38페이지 · 2분 42초). BOM 없이는 1000 이 먹는다(19페이지). 상세references/product-master.md.
⚠️⚠️ 판매(sale) — Updated 와 날짜 필드의 함정 (2026-08-29~31 실측)
- ⚠️
Updated는 「문서가 바뀌었다」는 뜻이 아니다. Cin7 이 내용 변화 없이 갱신한다. [실측 2026-08-28 11:25] 판매 문서 238건이 밀리초까지 같은Updated로 바뀌었는데 Cin7 History 에는 그 시각 활동이 하나도 없다(SO-15485마지막 활동 8/28 10:08 ·SO-13009는 8/21) ⇒ 플랫폼 쪽 일괄 갱신이다. 예고 없고 주기 미상. 📌 [08-26 유사 사례]PO-00754는 회계가 상태 라벨만 바꿔LastUpdatedDate가 갱신됐다. ⇒ 더 받는 방향이라 무해하지만, 대량이면UpdatedSince커서를 막는다(동률 그룹). - ⚠️⚠️
Fulfilments[].Ship.Lines[].ShipmentDate는 재고가 빠진 시각이 아니다. 사용자가 적는 날짜다. [실측SO-15041] Activity log 의Shipping for fulfillment #1 has been authorized= 08/20 16:29:21 인데ShipmentDate는 2026-08-21 이었다. Cin7 은 승인 시각에 차감한다. ⇒ 기준선 스냅샷 경계에서 이중 차감이 난다(2026-08-30 실사고 · 4문서 522행). 📌 실제 차감 시각은 Activity log(화면) 에만 있다 — API 에서 얻는 방법은 미확인. - ⚠️⚠️
ShipmentDate는 재고가 빠진 시각이 아니다 — 사용자 입력 날짜다. [실사고 2026-08-30 · 재발 09-01] 8/20 저녁 출하 승인으로 Cin7 이 차감했는데ShipmentDate는 8/21 이었다. 기초 스냅샷 경계에서 이중 차감이 난다. ⚠️ 재기준선을 잡을 때마다 걸린다. - ⚠️ Cin7 이 내용 변화 없이 대량 갱신한다 — [실측] 8/28(238건 · 밀리초 동일) · 8/31 오후. 사흘 간격이다. 그때 옛 문서가 재유입되므로 경계 문제가 되살아난다.
- 📌 판매 목록 행 키(실측):
SaleID, OrderNumber, Status, OrderDate, InvoiceDate, Customer, CustomerID, InvoiceNumber, CustomerReference, InvoiceAmount, PaidAmount, SaleInvoicesTotalAmount, InvoiceDueDate, ShipBy, BaseCurrency, CustomerCurrency, CreditNoteNumber, Updated, QuoteStatus, OrderStatus, CombinedPickingStatus, CombinedPaymentStatus, CombinedTrackingNumbers, CombinedPackingStatus, CombinedShippingStatus, CombinedInvoiceStatus, CombinedPaymentTotal, CreditNoteStatus, FulFilmentStatus, Type, SourceChannel, ExternalID, OrderLocationID, RestockStatus - 📌 상품 이동 내역은 화면이 가장 빠르다 — Cin7 Inventory 에서 상품을 열면 창고·bin 별
이동(Sale · Transfer out · PO 등)이 날짜순으로 나온다. [2026-08-31] 원장에 없는
TR-04330 −144를 이것으로 찾았다. API 로 같은 것을 얻는 방법은 미확인.
⚠️⚠️ 조립(Finished Goods) — VOIDED 가 다수다
- 목록 배열 키는
FinishedGoods다(FinishedGoodsList아님 · 2026-08-17 실측). ⚠️ 문서번호 필드는AssemblyNumber. - ⭐ 목록에
Status가 있다 — 상세 없이COMPLETED/VOIDED판정이 된다. - ⚠️ [실측 2026-09-01] 132건 중 VOIDED 86건. ⭐ 업무상 취소가 아니라 Cin7 의 기계적
동작이다. 기제 둘:
· ① SO 편집 → 재생성 — SO 를 고치면 기존 자동조립을 VOID 하고 새로 만든다.
Notes가by System인 것이 증거다. SO 묶음 30개가 「VOIDED → COMPLETED」 짝이다 (SO-15502:FG-00131VOIDED →FG-00132COMPLETED · 같은 날 · 같은 제품) · ② 트랜스퍼 픽용 SO 를 VOID — Asung 은 창고 간 트랜스퍼를 가상 손님ASUNG EDM TRANSFER의 SO 로 만들어 WMS 로 픽하고 끝나면 VOID 한다. [실증SO-14692] 딸린FG-00115~001206건이 전부 VOID 됐다 — 설계된 동작이다 - 📌 조립 자체는 월 20건 안팎이다. 월별로는 2025-10 의 16/16(완료 0건)만 이관 초기 노이즈이고 2026-03 이후도 61%로 꾸준하다(위 기제 때문).
- ⚠️ 수집 후 취소를 감지하지 않으면 원장이 틀어진다 — 실사고
FG-00131. ⭐ 다만 수집 주기 안에 상태 변화가 안 끝날 때만 걸린다(8월 12건 중 1건). - 📌 화면에
Undo버튼이 있다 — 완료된 조립도 되돌릴 수 있다. - 날짜 축이 셋이다:
Date(목록) ·CompletionDate·WIPDate.
⚠️⚠️ ManualJournals 는 발주와 트랜스퍼의 구조가 다르다 (2026-09-10 실측 · 같다고 가정한 사고)
발주 /advanced-purchase?ID= [PO-01111 · 15:06] |
트랜스퍼 /stockTransfer?TaskID= [TR-03975·03976·04175 · 14:40] |
|
|---|---|---|
ManualJournals[0] 키 |
4개 TaskID · InvoicingAndReceivingNumber · Status · Lines — ⭐ Lines 겹 안에 저널 |
11개 TaskID · ID · Reference · Amount · Date · Debit · Credit · ManualJournalsDistributedCosts · vDimensionDefaultValueStockTransferJournals · ValidationText · ValidationState — ⭐ 저널이 바로 원소 |
IsSystem |
실재 · Lines[0] Stock in Transit 91,877.51 Debit _59_ / Credit _1150040012_ true · Lines[1] 운임 90 Credit _135_ false |
⚠️⚠️ 없다 — 응답 전체 문자열 검색 0건(237K·95K·192K자). 운송중 계정 이동은 저널로 오지 않고 헤더 InTransitAccount 에 코드만 |
| In Transit 계정 | _1150040012_ |
_1150040007_ (헤더 InTransitAccount) |
| 비용 계정 | _135_ (landed) |
_136_ (운송비 · 화면 「Freight - COS」) |
| 걸러내는 조건 | IsSystem === false (위 PO 절 · 정상) |
Debit === '_59_' AND Credit === '_136_' (둘 다 화이트리스트) |
⬜→ ✅ [2026-09-11_135_·_136_계정명 미확인ref/account전량]_135_= Brokerage - COS(⚠️ 우리 문서의 "landed" — 통관중개료) ·_136_= Freight - COS · 둘 다 EXPENSE/ACTIVE. 필터엔 영향 없음.references/ref-endpoints.md.- ⚠️⚠️ [사고 2026-09-10] 트랜스퍼 프로브 출력을 요약하며 발주 구조를 섞어 「TR-03975 ARRAY(2) · [0]
IsSystem=true4878.11 · [1]IsSystem=false398.75」를 문서·주석·픽스처에 적었다 —4878.11은 응답에 없고_1150040007_은 헤더 오독. EF 필터j?.IsSystem === false가undefined === false로 전량 걸러져 다섯 회차docs_processed 0. ⇒ 응답 구조는 요약하지 말고 원문 JSON 을 옮긴다 · 픽스처는 실측 원문으로 · 두 엔드포인트를 같다고 가정하지 않는다. 정본:docs/design/ledger-design.md§원가 레이어 12번.
⚠️⚠️ 트랜스퍼 bin — API 는 주지 않는다 (2026-08-31 전수 확인)
정본: docs/sessions/2026-08-31-transfer-departure-bin.md
- 헤더에는 있다:
FromLocation/ToLocation이"창고: bin"형태다 (같은 창고 안 bin 이동은 이것으로 충분하다). ⚠️ 창고 간 이동은 창고 이름만 나온다. - ⚠️⚠️ 라인에는 없다.
stockTransfer→Lines· 같은 응답의Order.Lines·stockTransfer/order→Lines셋이 키까지 완전히 동일하고 bin 이 없다. 공식dearinventory.apib의 Stock Transfer Line Model 에Bin·Location정의 자체가 없다. - ⚠️⚠️ 재고 이동(movement) 조회 API 가 존재하지 않는다 — 엔드포인트 102개 전수 확인.
재고 계열은
stockadjustment·stocktake·stockTransfer·ref/productavailability(현재 상태) ·transactions(회계 분개 — 수량·bin 없음)뿐이다. 📌 추측한ref/stockMovementDetails류는 HTTP 200 + HTML 404 페이지를 준다 — JSON 이 아니면 파싱 전에 걸러라. - 📌 필드 이름이 둘이다:
BinID(생산 계열 — Disassembly · Finished Goods · Inventory Write-Off) vsLocation(입출고 — 예:PutAway.Lines.Location). 그리고 Cin7 에서Location은 트리다 — bin 도 Location 이다 (ref/location2,676건 ·ParentID로 창고에 매달림). - ⚠️
ref/productavailability에는Bin이 있다(현재 재고 · bin 단위). 기초 스냅샷이 이미 bin 단위인 이유이고, bin 단위 대조가 가능한 근거다. - ⭐ 문서 데이터는 화면
ExportCSV 에 있다 — 라인별Location. [결정적 실측TR-04166] CSV 가 출발 binE050202를 주는데 그 제품은 지금E050103에 있다 ⇒ 기록값이다. ⚠️ 반면 화면의LOCATION컬럼은 현재 재고 조회다 (QuantityOnHand처럼 참고값). 화면과 Export 를 혼동하지 말 것. - ⚠️ 픽용 SO 는 VOID 하면
Pick/Pack/Shiplines 가 0이 된다(실측SO-15482). 문서는 남지만 픽 데이터는 사라진다. - 📌
Reference가"WMS putaway …"로 시작하면 우리 WMS 가 API 로 만든 문서다 (User = "Data Management (API Application)"). 픽이 아니라 SO 가 없는 것이 정상이다.
⚠️ Advanced Purchase 상세 — 원가(COGS)를 읽을 때 (2026-08-27 실측)
정본: docs/sessions/2026-08-27-landed-cost-investigation.md · 구현: EF inv-cost
GET /advanced-purchase?ID=<purchaseList 의 ID>⚠️/purchase는 Advanced·Service Purchase 를 지원하지 않는다 —"This endpoint is deprecated and does not support Advanced Purchase and Service Purchase". ⚠️ 파라미터는ID다.TaskID는"Purchase Task with specified ID not found".- ⚠️⚠️
Invoice·StockReceived·PutAway·CreditNote·ManualJournals는 전부 배열이다.[0]만 보면 틀린다 —PO-01130은 Invoice 2 · SR 2 · PA 2 였고, 첫 원소만 보다가 숫자가 안 닫혀 헤맸다. - ⭐
InvoicingAndReceivingNumber(I&R) 가 회차↔인보이스 대응 축이다. 세 배열 모두에 있다. 입고 회차마다 환율이 다르므로(PO-011301.40275 / 1.39342) 이 축으로 맞춰야 한다. - ⚠️⚠️ SR 블록의
Status를 판정에 쓰지 말 것. [실측]PO-01117SRstatus="DRAFT"인데 SR qty 8,664 = PA qty 8,664 이고 COGS 도 오차 0으로 맞았다(헤더는AUTHORISED·FULLY RECEIVED). Advanced 의 확정 축은PutAway다. SR 은 VOIDED 만 제외한다. - ⚠️⚠️
Invoice.Total은 세후다. 재고 원가 계산에는TotalBeforeTax를 쓴다 ([실측] 국내 매입PO-00967에서 HST 13% 만큼 어긋났다. 수입 PO 는TaxRule="Zero-rated (Purchase)"라Tax=0이어서 오래 안 보였다). InventoryMovements= 가치 장부다.ProductID · Date · COGS뿐 — 수량도 SKU 도 bin 도 없다. ⚠️ 같은(ProductID, Date)에 행이 여럿일 수 있다(재평가 상쇄+A/−A/+B) — 합산해야 순액이다. ⚠️ bin 은PutAway.Lines에만 있다 —StockReceived.Lines의Location은 전부 null 이다. ⚠️ Advanced 한정 — Simple 은 정반대다(아래 절 · 2026-09-09).Type='Service Purchase'(IsServiceOnly=true) 는 통관사·포워더 인보이스 문서다.InventoryMovements0건이고 자기가 어느 PO 에 붙었는지 모른다(연결 필드 없음). ⇒ 원가는 반드시 Advanced PO 축에서 읽는다. 목록의IsServiceOnly로 걸러진다([실측] 41%). 📌 PO 쪽ManualJournals의IsSystem=false줄이 그 비용이고, 금액은 세전액이다 (HST 13% 제외 — 4건 전수 확인). 한 인보이스가 여러 PO·여러 줄로 쪼개진다. ⚠️⚠️ [정정 2026-09-09] 「어느 PO 에 붙었는지 모른다」는 API 관점의 반쪽이었다. Cin7 안에서는 북키퍼가 Service 인보이스의Expense버튼으로 해당 PO 를 찾아 수동 링크한다(Caleb 확인). 연결은 실재하고 사람이 만든다. ⇒ 위 「PO 쪽ManualJournalsIsSystem=false줄 · 한 인보이스가 여러 PO·여러 줄로」가 바로 이 수동 링크의 결과다. 방향은 PO → Service 한쪽 — Service 에서 역참조는 API 로 못 읽지만 PO 에서 「어떤 비용이 붙었나」는 읽힌다 ⇒ 되짚기 경로가 있다. ⚠️ 소급 타이밍은 사람에게 달려 있다 —asung-inv-ledger§하드 플립 판단 (라) 참조. 📌 통화 축 (Caleb 확인 2026-09-09): USD 결제 운임은 Service 인보이스에서 CAD 로 환산되어 넘어온다. 환산 환율은 Service 인보이스 작성 시점 기준이고 그 환율은 Service Purchase 문서 최상위(SupplierCurrency·CurrencyRate)에 보존된다([실측PO-01268] CAD/1.0). ⇒ 우리가 PO 축에서 읽는IM.COGS는 환산 이후의 CAD 확정값이고, 그래서inv_cost의landed행은currency_orig·fx_rate·amount_orig가 전량 null 인 것이 정확한 표현이다(원문 통화가 우리 쪽에 도달하지 않는다). [실측]landed434/434 전량 null — 종전 「표본 4건이 CAD 였다」가 우연이 아니라 경로 자체가 항상 환산된 CAD 를 준다.- ⭐
ManualJournals줄의Date가 goods/landed 분류를 결정한다 (2026-09-09 실측PO-01120).IsSystem=false줄의Date가 입고일과 다르면 별도 IM 묶음이 되어landed로 잡히고, 같으면 재고 본체와 합산되어goods에 섞인다. [실물] 운임 ① 90.00(Date 08-27·Ref 16895) → IM 08-27 묶음 COGS 89.999982 ⇒landed/ 운임 ② 695.00(Date 08-28·Ref 42256) → IM 08-28 묶음 COGS 22,395.686012 = 21,700.69 + 695 ⇒ goods 에 병합(inv-cost의warn_merged_landed). ⚠️ IM 에는ProductID+Date축만 있어 분리할 수단이 없다 — 우리가 고칠 수 없고 경고가 그 사실을 알린다. 📌 그 날짜는 북키퍼가 입력한다. 정본docs/sessions/2026-09-09-simple-purchase-cost.md§7-①. - ⚠️ 금액 일치를 문서 연결의 근거로 쓰지 말 것 — [09-09]
PO-01120차액 695 가PO-01268(Service Purchase) 금액 695 와 같아 같은 건으로 추측했으나 틀렸다(Ref 42256≠ 그 문서). 695 는 흔한 운임 금액이다. - 배분은 금액 비례다(수량·무게 아님). 분모 =
Invoice.Lines총합(AdditionalCharges 전).
⚠️ Simple Purchase 상세 — Advanced 와 정반대인 축들 (2026-09-09 실측)
정본: docs/sessions/2026-09-09-simple-purchase-cost.md · 구현: EF inv-cost(Simple 분기) ·
표본: PO-01215(47라인 9,192개 · USD · GAS 프로브 3회)
- 주소는
GET /purchase?ID=— ⚠️/advanced-purchase로 부르면 빈 껍데기다(200 · 조용함). POST/PUT 이면 PO 가 조용히 Advanced 로 변환된다(주의 13번). 읽기라도 주소를 섞지 말 것. - 최상위 블록:
Order(객체) ·StockReceived(객체) ·Invoice(객체) ·CreditNote(객체) ·ManualJournals(객체) ·InventoryMovements(배열). ⚠️PutAway블록이 ABSENT — Advanced 와 다르다. - ⚠️⚠️ bin 이
StockReceived.Lines[].Location에 있다 — Advanced 와 정반대다. Advanced 는 SR 의Location이 null 이고 bin 이PutAway에만 있다. [실측]Locationnull 0건 · 18개 bin. 📌 SR 라인 키:Date, Quantity, ProductID, SKU, Name, Location, LocationID, Received, BatchSN, SupplierSKU, ExpiryDate, CardID, …—CardID도 있다(원장line_ref와 그대로 맞는다). ⚠️ProductCustomField2는 상품 마스터 필드 — bin 처럼 보여도 쓰지 말 것. - ⚠️⚠️
Invoice.CurrencyRate가 null 이다 — 환율은 응답 최상위CurrencyRate뿐이다([실측] 1.37905). Advanced 는 인보이스 블록마다 환율이 다르므로 회차 값 우선 · 헤더 폴백이 맞는 순서다. - ⚠️
InvoicingAndReceivingNumber(I&R)가 없다 — Invoice·SR·CreditNote·ManualJournals·최상위 전부 없고, SR/Invoice 블록에TaskID자체가 없다.IM[0].TaskID= PO ID 다(Advanced 는 하위 태스크 GUID) ⇒ 단일 태스크 구조. InventoryMovements는 Advanced 와 동일(TaskID, ProductID, Date, COGS, …· 수량·SKU·bin 없음). ⭐ [실측 검산]sum(COGS) = 35,957.0601=Invoice.TotalBeforeTax 26,073.79 × 최상위 CurrencyRate 1.37905— 소수점까지 일치. ⇒ ⭐ 환율 환산도 할인 배분도 계산 불필요 — Cin7 확정 CAD 값이다.- ⚠️⚠️
Order.Lines를 쓰지 말 것 — [실측] 52라인 9,840개 ≠ SR/Invoice 47라인 9,192개(백오더 5라인 648개). 오더 축으로 기대치를 잡으면 전부 가짜 결손이 된다(PO-01068Advanced 전례와 같은 계열). 📌 Simple 은 오더 라인이d.Lines가 아니라d.Order.Lines안에 있다. - ⚠️
AdditionalCharges를 따로 더하지 말 것 — [실측] Co-op 4%-1086.41·Account="_59_"(재고 축)이고 이미COGS에 반영돼 있다(Invoice.Lines합 27,160.20 − 1,086.41 =TotalBeforeTax26,073.79). 이중 계상. - 📌
Invoice.Total이 아니라TotalBeforeTax— 이 표본은Tax=0(Zero-rated (Purchase))이라 우연히 같지만 국내 매입은 HST 13% 만큼 어긋난다. - 📌 Service Purchase 는 금액만 있고 라인이 없다 — [실측
PO-01268]Invoice.Lines·InventoryMovements·ManualJournals.Lines전부 빈 배열이고TotalBeforeTax만 있다(695) ⇒ 목록의IsServiceOnly로 거르는 것이 맞다. Invoice[].AdditionalCharges의Account가 재고 여부를 가른다 —_59_(Discount·Rounding)는 재고,_95_(Freight·Commission)는 손익. ⚠️Order.AdditionalCharges에는Account필드가 없다 — 판정은Invoice쪽으로.