[API 튜토리얼] 상품명만 넣으면 관세율까지 — HS코드 자동분류 API 사용법
작성 2026-08-18 · 갱신 2026-08-18
“나이키 운동화”처럼 사람이 쓰는 말을 그대로 넣으면 HS코드 후보와 국가별 관세율을 함께 돌려주는 API가 있다. GET /api/v1/classify 하나로 끝난다. 이 글은 그 엔드포인트의 실제 스펙(docs/openapi-classify.yaml)과 구현(src/pages/api/v1/classify.ts)을 그대로 옮긴 사용법이다.
요청
GET https://duty.pix-make.com/api/v1/classify?q=무선+이어폰&limit=3
| 파라미터 | 필수 | 설명 |
|---|---|---|
q |
예 | 자연어 상품명, 최대 200자. 숫자 4~10자리(HS코드)를 그대로 넣어도 된다 |
lang |
아니오 | 응답 언어. ko(기본)·ja·en |
countries |
아니오 | 콤마로 구분한 국가 코드. 기본값은 kr(원산지 협정 없이 적용되는 기본세율·WTO양허세율). 그 외 us, eu, cn, gb, vn, jp, asean, au, ca, in, sg, tr, nz, cl은 대한민국이 체결한 FTA·RCEP 상대국이다. 지원하지 않는 코드를 넣으면 조용히 무시되고 응답의 meta.unsupported_countries에 표시된다 |
limit |
아니오 | 반환할 후보 개수. 1~5, 기본값 5. 범위를 벗어나면 기본값으로 대체된다 |
인증은 헤더로 한다. X-RapidAPI-Proxy-Secret 또는 X-Internal-Key 둘 중 하나가 필요하다.
curl -s "https://duty.pix-make.com/api/v1/classify?q=%EB%AC%B4%EC%84%A0%20%EC%9D%B4%EC%96%B4%ED%8F%B0" \
-H "X-Internal-Key: <구독 후 발급되는 키>"
응답
{
"query": "무선 이어폰",
"normalized_query": "이어폰",
"candidates": [
{
"rank": 1,
"hs_code": "8518300000",
"title": { "ko": "그 밖의 것[헤드폰ㆍ이어폰]", "ja": null, "en": null },
"score": 0.87,
"tariffs": [
{
"country": "kr",
"rates": [
{ "kind": "basic", "rate": "8%", "label_ko": "기본세율" }
]
}
]
}
],
"meta": {
"limit": 5,
"unsupported_countries": [],
"disclaimer": "score는 검색 순위 점수(RRF 정규화)이며 분류 정확도 확률이 아님. 실측: top1 64% / top5 재현율 93%. 법적 효력 없음, 관세사 확인 필요."
}
}
normalized_query는 브랜드명·모델명을 제거하고 일반 명사로 바꾼 질의다. 숫자 코드로 직접 질의하면 이 값은 원문과 같다. title.en은 계층 제목의 영문판이 원본 데이터에 없어 항상 null로 온다 — 빈 문자열 대신 정직하게 null을 준다.
score는 정확도가 아니다
각 후보의 score는 검색 순위를 RRF(Reciprocal Rank Fusion) 방식으로 0~1 사이 값으로 정규화한 것이다(감쇠 상수 k=60). 분류가 맞을 확률이 아니라 검색 결과 안에서의 상대적 순위를 나타낼 뿐이다. 그래서 모든 응답에 meta.disclaimer로 실제 정확도 실측치를 함께 준다: 정답 세트 28건 기준 top1 정확도 64%, top5 재현율 93%. 1위 후보가 정답이 아닐 수 있다는 뜻이므로, 중요한 판단에는 limit을 늘려 상위 몇 개를 함께 확인하는 편이 안전하다.
검색 방식과 호출 비용
질의는 벡터 임베딩(bge-m3)으로 변환돼 Vectorize에서 의미 유사 품목을 찾고, 이 결과가 LIKE 검색 결과·질의 재작성(브랜드명 제거) 결과와 RRF로 합쳐져 최종 후보가 정해진다.
이 과정에서 Workers AI 호출은 질의 유형에 따라 달라진다.
| 질의 유형 | AI 호출 횟수 |
|---|---|
| 숫자 4~10자리(HS코드 직접 입력) | 0회 |
| 일반 자연어 상품명 | 최대 3회(질의 재작성 1회 + bge-m3 임베딩 최대 2회) |
재작성된 질의가 원문과 같거나 재작성이 실패하면 임베딩 호출은 1회로 줄어든다. 결과 캐시(1일)·AI 캐시(7일)가 적중하면 실제 호출 수는 더 줄어든다. 즉 같은 질의를 반복해서 보내는 워크플로라면 캐시가 비용을 상당 부분 흡수한다.
국가 코드와 관세율 종류
countries=kr,jp처럼 여러 국가를 한 번에 요청하면 후보마다 국가별 tariffs 배열이 함께 온다. kr은 기본세율·WTO양허세율(basic/wto)을 뜻하고, 그 외 국가 코드는 해당 국가와 체결된 협정세율에 대응한다. 협정세율은 원산지증명서 요건이 붙는다는 점을 감안해서 봐야 한다 — 기본세율과 같은 자리에 나온다고 해서 조건 없이 적용되는 세율은 아니다.
에러 처리
| 상태 코드 | error.code |
상황 |
|---|---|---|
| 400 | missing_query / query_too_long |
q 누락 또는 200자 초과 |
| 401 | unauthorized |
인증 헤더 누락 또는 값 불일치 |
| 405 | method_not_allowed |
GET 이외의 메서드로 호출 |
| 500 | internal_error |
서버 내부 오류(원인은 서버 로그에만 남고 응답에는 노출하지 않는다) |
| 503 | not_configured |
인증 시크릿이 아직 서버에 배포되지 않은 상태 |
401과 503을 구분해서 응답하는 이유가 있다. 401은 “키가 틀렸다”이고 503은 “서버가 아직 인증을 받을 준비가 안 됐다”이다. 이 둘을 같은 코드로 묶으면 호출하는 쪽에서 재시도해야 할지 키를 다시 발급받아야 할지 구분할 수 없다.
정리
이 API는 duty-site 화면(search.astro)이 쓰는 것과 완전히 동일한 검색 로직(searchItems())을 그대로 호출한다. 화면과 API가 다른 결과를 낼 일이 없고, 화면에서 검증된 정확도(top1 64%, top5 재현율 93%)가 API 응답에도 그대로 적용된다. score는 순위 지표일 뿐 확신도가 아니라는 점, 그리고 실제 세액 계산에는 관세사·세관 확인이 필요하다는 점만 기억하면 된다.
- duty-site API 스펙 원문 — docs/openapi-classify.yaml
- duty-site API 구현 — src/pages/api/v1/classify.ts