Reference

배송조회 API

운송장번호 하나로 국내외 13개 택배사의 배송 상태를 조회합니다. 택배사 코드를 몰라도 11개 택배사가 자동 감지 대상입니다. 국내 주요 4사는 1순위 적중률 98.6% 이상, EMS·해외직구는 국제 형식이면 접두어로, 나머지 5개는 학습 표본이 적어 1순위를 놓칠 수 있습니다 (등급별 실측).

https://taekbae.net/v1 REST · JSON OpenAPI 3.1

빠른 시작

키를 발급받고 한 번 호출하기까지 두 단계입니다.

  1. 내 계정 › API 관리 에서 API 키를 발급합니다.
  2. 운송장번호를 경로에 넣어 호출합니다.
요청
curl https://taekbae.net/v1/tracking/12345678901 \
  -H "Authorization: Bearer $TAEKBAE_API_KEY"

문서의 12345678901은 실제 배송 건이 아닌 예시용 번호입니다. 그대로 실행하면 자동 감지가 후보를 특정하지 못해 422 carrier_not_detected가 돌아옵니다(후보로 재시도하는 법). 아래 200 본문은 실제 배송 건을 조회했을 때의 모양이니, 손에 있는 운송장번호로 바꿔 실행하세요.

응답 200
{
  "tracking_number": "12345678901",
  "carrier":  { "code": "cj", "name": "CJ대한통운", "detected": true },
  "status":   { "code": "linehaul", "label": "간선 이동 중", "is_final": false },
  "last_event_at": "2026-07-26T09:12:00+09:00",
  "events": [
    {
      "occurred_at": "2026-07-26T09:12:00+09:00",
      "occurred_at_raw": "2026-07-26 09:12:00",
      "time_precision": "exact",
      "location": "대전HUB",
      "status_text": "간선상차",
      "stage": "linehaul"
    }
  ],
  "as_of": "2026-07-26T18:06:00+09:00"
}

eta 키가 아예 없습니다. null이 아니라 키 자체가 빠지는 것이 이 API의 규칙입니다.

필드설명
tracking_number정규화된 운송장번호. 하이픈을 지우고 영문을 대문자로 올린 값이라 보낸 값과 다를 수 있습니다.
carriercode·name으로 택배사를 식별합니다. "detected": true자동 감지된 값입니다(요청에 carrier를 넣지 않은 경우). "detected": false요청에서 지정한 값 그대로라는 뜻입니다(예: ?carrier=cj로 요청하면 필드가 빠지지 않고 그대로 false가 옵니다). 운송장번호는 택배사마다 따로 발급되는 값이라, 드물지만 서로 다른 택배사가 우연히 같은 번호로 실제 배송 건을 각자 갖고 있을 수 있습니다. 그러면 추론이 맞았다고 믿어도 남의 화물일 수 있으니, true일 때는 사용자에게 "다른 택배사인가요?"를 물을 근거로 쓰세요.
status분기는 code로 하세요(label은 화면 문구라 바뀔 수 있습니다). is_finaltrue면 배송완료·반송·취소처럼 더 이상 바뀌지 않는 상태이므로, 폴링 종료 조건으로 쓰세요.
last_event_at가장 최근 이벤트의 발생 시각. 변화 감지에 쓰기 좋습니다.
events[]배송 이력. 최신순이 아니라 발생 순(오래된 것부터)입니다. 각 항목의 stagestatus.code와 같은 값 집합(분기용), status_text는 택배사가 준 원문 문구(화면용), occurred_at은 발생 시각입니다.
eta BETA도착 예측. ?eta=true로 요청했을 때만 옵니다. confidencelow고객에게 그대로 약속하지 말고 참고값으로만 쓰세요.
as_of택배사에서 정보를 가져온 시각. 응답을 만든 시각이 아닙니다.

위는 자주 보는 필드만 간추린 것입니다. 전체 필드와 parties·delivery_agent 등 하위 객체는 응답 필드 전체 보기 →

도착 예측(eta)은 기본으로 오지 않습니다. 아직 정확도가 충분하지 않아 옵트인으로 두었습니다. 달라고 하지 않은 응답에 실어 보내면 그 값을 그대로 믿고 고객에게 도착 시간을 안내하거나 늦었는지 판단하는 데 쓰게 되기 때문입니다. 필요하면 ?eta=true로 켜되, confidencesupport_count를 함께 읽고 참고값으로만 쓰세요.

as_of는 응답을 만든 시각이 아니라 택배사에서 정보를 가져온 시각입니다. 캐시된 응답을 받으면 이 값이 조금 과거일 수 있고, 그게 정확한 표현입니다.

판매자가 등록만 해 두고 아직 택배사에 넘기지 않은 운송장("가송장")도 이 API에서는 정상 응답으로 옵니다. tracking_number_not_found가 아니라 status.codeaccepted인 채로 며칠씩 멈춰 있는 식입니다. 가송장 구분하는 법 →

AI에게 시키기

코드를 직접 쓰지 않고 AI 코딩 도구에 맡길 수도 있습니다. 길게 설명할 필요가 없습니다. 엔드포인트·필드·오류 코드는 물론 "폴링은 is_final로 멈춰라", "date_only면 00:00으로 읽지 마라" 같은 주의사항까지 스펙 설명 안에 들어 있습니다.

시작 전에 준비할 것

  1. OpenAPI 스펙 주소: https://taekbae.net/v1/openapi.json
    AI가 이 주소만 읽으면 엔드포인트·필드·오류 코드를 전부 알 수 있습니다. 문서를 붙여 넣을 필요가 없습니다.
  2. API 키: 내 계정 › API 관리 에서 발급합니다. 평문은 발급 직후 한 번 보이고, 이후에는 목록에서 열람할 수 있습니다.

이렇게 지시하세요

AI 코딩 도구에 붙여 넣기
https://taekbae.net/v1/openapi.json을 읽고, 이 스펙을 기준으로 택배 배송조회를
지금 프로젝트에 구현해 주세요. 배송의 어떤 상태를 어떻게 보여줄지는 아래 요구에
맞춰 정하고, 구현하는 범위 안에서는 스펙의 엔드포인트·필드·오류 코드·주의사항을
빠뜨리지 말아 주세요.

요구: (예: 주문 목록에 현재 배송 상태만 배지로 표시)

문서를 요약해서 붙여 넣지 말고, 위 OpenAPI 스펙 주소를 그대로 주세요. 요약은 만드는 순간의 스냅샷이라 그 뒤에 스펙이 바뀌면 안 맞게 됩니다. 스펙 주소는 서버가 지금 상태를 그대로 만들어 내보내므로, 언제 열어도 실제 동작과 같습니다.

스펙 전체를 다 구현할 필요는 없습니다. 배송 상태만 배지로 보여 주면 되는 화면에 이동 이력·예상 도착까지 만들면 군더더기가 됩니다. 필요한 화면을 «요구»에 적어 범위를 좁히세요. 다만 만들기로 한 범위 안에서는 오류 코드와 주의사항을 빼지 마세요. 자동 감지 실패(422)나 한도 초과(429) 처리를 건너뛰면 평소에는 잘 되다가 실제 상황에서 무너집니다.

도구로 먼저 호출해 보기

코드를 짜기 전에 터미널에서 한 번 찔러 보는 쪽이 빠릅니다. 키가 맞는지, 자동 감지가 되는지, 응답이 어떤 모양인지 여기서 확인하세요.

# macOS · Linux · WSL
export TAEKBAE_API_KEY="tb_xxxxxxxxxxxxxxxxxxxx"

# 기본 조회 (택배사 자동 감지, ETA 없음)
curl -s "https://taekbae.net/v1/tracking/12345678901" \
  -H "Authorization: Bearer $TAEKBAE_API_KEY" | jq

# 택배사를 지정하고 최신 이벤트만
curl -s "https://taekbae.net/v1/tracking/12345678901?carrier=cj&events=latest" \
  -H "Authorization: Bearer $TAEKBAE_API_KEY" | jq

# 도착 예측까지 (기본은 오지 않습니다)
curl -s "https://taekbae.net/v1/tracking/12345678901?eta=true" \
  -H "Authorization: Bearer $TAEKBAE_API_KEY" | jq '.eta'

# 상태코드와 헤더까지 보기: ETag·RateLimit-Remaining을 확인할 때
curl -i -s "https://taekbae.net/v1/tracking/12345678901" \
  -H "Authorization: Bearer $TAEKBAE_API_KEY" | head -20

12345678901은 예시용 번호입니다. 그대로 실행하면 자동 감지가 후보를 특정하지 못해 422 carrier_not_detected가 돌아옵니다. 손에 있는 운송장번호로 바꿔 실행하세요.

코드로 붙이기

조회 한 번과 상태별 분기를 담은 최소 예제입니다. 오류를 code로 분기하는 부분이 핵심입니다. title 문구는 바뀔 수 있지만 code는 유지됩니다.

일곱 예제는 같은 계약을 구현합니다. tracking_number_not_found·carrier_not_detected·quota_exceeded·carrier_unavailable·carrier_error를 모두 분기하고, 304(변화 없음)와 404(운송장 없음)를 서로 다른 반환 채널로 돌려주며, 429·503Retry-After를 호출자에게 전달합니다. 조회에 성공하면 본문과 함께 ETag라는 일종의 "이 결과의 버전 값"도 돌려줍니다. 다음 조회 때 이 값을 그대로 실어 보내면(If-None-Match 헤더), 그 사이 내용이 안 바뀌었을 때 서버가 본문 없이 짧게 "그대로예요"(304)라고만 답해서 더 가볍고 빠릅니다. 자세한 사용법은 캐시와 폴링을 보세요.

# pip install requests==2.34.2   (표준 라이브러리만으로는 돌지 않습니다)
import os, requests

BASE = "https://taekbae.net/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['TAEKBAE_API_KEY']}"


class TrackError(Exception):
    """API 오류의 뿌리. retry_after는 429·503에서만 채워진다."""
    def __init__(self, message, retry_after=None):
        super().__init__(message)
        self.retry_after = retry_after


class NotFound(TrackError): """404. 그 운송장이 택배사에 없다(304와 다르다)."""
class CarrierNotDetected(TrackError): """422. 후보가 없다. carrier를 지정해야 한다."""
class QuotaExceeded(TrackError): """429. retry_after만큼 기다린다."""
class CarrierUnavailable(TrackError): """503. 택배사 장애(운송장이 없는 것과 다르다)."""
class CarrierError(TrackError): """502. 상류 실패. 재시도 가능하고 차감도 없다."""


def track(number, carrier=None, etag=None):
    """(결과, ETag)를 돌려준다. 내용이 그대로면 결과는 None이고 ETag는 유지된다."""
    r = SESSION.get(f"{BASE}/tracking/{number}",
                    params={"carrier": carrier} if carrier else {},
                    headers={"If-None-Match": etag} if etag else {},
                    timeout=15)

    if r.status_code == 304:               # 변화 없음. 쿼터 차감 없음
        return None, etag
    if r.status_code == 200:
        return r.json(), r.headers.get("ETag")

    problem = r.json()
    code, title, rid = problem["code"], problem["title"], problem["request_id"]
    retry_after = r.headers.get("Retry-After")
    retry_after = int(retry_after) if retry_after else None

    if code == "tracking_number_not_found":
        raise NotFound(title)                                    # 차감된다. 폴링을 멈춘다
    if code == "carrier_not_detected":
        # candidates에는 **아직 시도하지 않은** 후보만 온다. 비어 있으면
        # 더 해볼 게 없으니 사용자에게 택배사를 물어야 한다.
        candidates = problem.get("candidates") or []
        if not candidates:
            return None, None
        return track(tracking_number, carrier=candidates[0]["code"])
    if code == "quota_exceeded":
        raise QuotaExceeded(title, retry_after)
    if code == "carrier_unavailable":
        raise CarrierUnavailable(title, retry_after)
    if code == "carrier_error":
        raise CarrierError(f"{title} (request_id={rid})")         # 재시도 가능
    raise TrackError(f"{code}: {title} (request_id={rid})")


try:
    result, etag = track("12345678901", carrier="cj")
    if result is None:
        print("변화 없음")                    # 304. 가지고 있던 결과를 그대로 쓴다
    else:
        print(result["status"]["label"], result["status"]["is_final"])
except NotFound:
    print("아직 등록되지 않은 운송장입니다")   # 404를 '변화 없음'으로 읽지 않는다

같은 운송장을 반복해서 조회해야 한다면(배송이 끝날 때까지 폴링) 배송 상태 계속 확인하기를 먼저 보세요.

인증

모든 요청에 Authorization 헤더가 필요합니다.

Authorization: Bearer tb_xxxxxxxxxxxxxxxxxxxx

쿼리스트링으로는 키를 받지 않습니다. URL에 실린 키는 액세스 로그·프록시 로그·Referer 헤더에 그대로 남기 때문입니다. 키가 노출됐다면 계정에서 즉시 폐기하고 새로 발급하세요.

API 키는 암호화해 저장합니다. API 관리 권한이 있는 프로젝트 구성원은 내 계정 › API 관리에서 키 전체를 다시 확인할 수 있으므로, 잃어버렸다고 바로 재발급할 필요는 없습니다. 다만 콘솔에 접근할 수 있는 사람은 키도 볼 수 있으니 구성원과 로그인 계정을 신중하게 관리하세요.

택배사 자동 감지

carrier를 생략하면 운송장번호만으로 택배사를 추론합니다. 국제 운송장은 접두어가 결정적이고(EMS의 S10 형식, 해외직구의 LP…CN), 국내 번호는 학습된 형식 패턴을 씁니다.

curl https://taekbae.net/v1/tracking/12345678901 \
  -H "Authorization: Bearer $TAEKBAE_API_KEY"

조회에 성공하면 응답의 carrier.detectedtrue로 옵니다. 우리가 추론한 값이라는 뜻입니다. 이 값으로 "자동 감지를 믿을지, 사용자에게 택배사를 물을지" 판단할 수 있습니다. 감지에 실패하면 아래처럼 422가 오므로, 두 경우를 모두 처리해야 합니다.

해외 배송은 번호가 국내 구간에서 바뀝니다. EE…KR(EMS)나 LP…CN(해외직구) 같은 국제 형식은 접두어로 바로 판별되지만, 국내 인계 후 받는 번호는 그냥 국내 형식의 숫자라 국내 규칙을 탑니다. 해외 주문에서 받은 번호가 422로 돌아온다면 대개 이 경우이니, 후보 목록을 쓰거나 carrier를 지정하세요.

얼마나 믿을 수 있나

자동 감지 대상은 11개입니다. unsupported인 CU편의점택배·DHL 둘만 빠집니다. 다만 방식과 신뢰도가 셋으로 갈립니다. 국내 4사는 학습된 번호 형식으로 추론하고, EMS·해외직구는 접두어로 1순위를 잡아 학습과 무관하며, 나머지 5개는 감지 대상이긴 하나 학습 표본이 부족합니다. 아래 적중률은 학습 방식에만 해당합니다(2026-07-26 실측, 정답 9,542건).

등급대상1순위 적중률
reliable CJ대한통운 · 롯데택배 · 한진택배 · 우체국택배 98.6 ~ 100%
prefix 우체국 EMS · 알리익스프레스 국제 형식이면 1순위 확정 (학습 무관)*
weak 로젠 · GS25 · 대신 · 경동 · SLX 학습 표본 부족 (3~814건)
unsupported CU편의점택배 · DHL 감지 대상 아님

자동 감지는 기본 사용법이 아니라 안전망입니다. 택배사를 아신다면 carrier를 지정하세요. 학습 표본의 98%가 국내 주요 4사에 몰려 있어서, 소형 택배사 물량이 많다면 자동 감지에 의존하지 마세요.

감지에 실패해도 다른 택배사로 대신 조회하지 않습니다. 실측에서 2순위 후보가 결과를 구해낸 건이 0건이었습니다. 1순위가 빗나가는 경우는 대개 그 번호가 어디에도 없는 것이라 두 번째도 똑같이 실패합니다. 재시도는 정확도를 못 올리면서 지연만 두 배로 만들기 때문에, 후보 목록을 돌려주고 재시도 여부는 호출자가 정하도록 했습니다.

* 접두어는 1순위를 정할 뿐, 조회 성공을 보장하지 않습니다. 국제 구간을 지난 화물이 국내에서 우체국으로 인계되는 경우가 있어, 접두어로 잡은 1순위(EE…KR→우체국 EMS, LP…CN→알리익스프레스)가 정보를 못 내놓으면 우체국택배가 후보로 돌아옵니다. 즉 국제 형식 번호라도 상류에 없으면 404가 아니라 422(tried 1개 + candidates 1개)가 올 수 있습니다.

등급은 GET /v1/carriersauto_detect로도 받을 수 있습니다. 택배사별 표에서 확인하세요.

특정하지 못하면 후보와 함께 422가 옵니다. 막다른 오류가 아니라 다음 행동이 있는 오류입니다. 13개 전체 목록 대신 남은 후보만 보여주면 됩니다.

응답 422
{
  "type": "https://taekbae.net/docs/api/errors/carrier_not_detected",
  "title": "운송장번호로 택배사를 특정하지 못했습니다. carrier 파라미터로 지정해 주세요",
  "code": "carrier_not_detected",
  "status": 422,
  "detected": true,
  "tried":      [ { "code": "cj",     "name": "CJ대한통운" } ],
  "candidates": [ { "code": "lotte",  "name": "롯데택배" },
                  { "code": "hanjin", "name": "한진택배" } ],
  "request_id": "req_a91a852649404caea0c13bfd4943182f"
}

candidates는 아직 시도하지 않은 후보만 담고, 빌 수 있습니다. tried는 이번 요청에서 실제로 조회했으나 정보가 없었던 택배사라 같은 값으로 재시도하면 반드시 404가 납니다. 후보를 다 썼거나 감지 모델이 후보를 하나도 못 뽑으면 candidates[]이니, 인덱싱 전에 길이를 확인하세요. 그때는 택배사 목록에서 사용자가 직접 고르게 하는 것이 유일한 다음 행동입니다.

자동 감지 조회도 호출은 1건입니다. 택배사를 실제로 부른 이상 결과가 200이든 422든 똑같이 1건을 셉니다. 같은 크롤이 carrier 생략 여부로 공짜가 되면 안 되니까요. 반대로 후보를 하나도 못 뽑아 택배사를 아예 부르지 않은 422(tried: [])는 차감하지 않습니다.

집화 확인 (가송장 방지)

쇼핑몰에서 판매자가 운송장번호를 등록했지만 실제로는 택배사에 물건을 넘기지 않은 경우를 잡는 패턴입니다. 배송 완료까지 추적할 필요 없이 "택배사가 물건을 인수했는가" 하나만 보면 됩니다.

어떤 필드를 보나

events[].stage를 봅니다. status.code만 보면 안 됩니다. 그 값은 지금 상태라, 이미 간선으로 넘어간 건은 linehaul이지 picked_up이 아닙니다. "집화를 지났는가"는 이력 전체에서 찾아야 합니다.

stage인수됐나
accepted접수. 운송장은 발급됐지만 아직 물건을 받지 않은 상태아니오
picked_up집화완료. 택배사가 물건을 실제로 인수
at_origin_terminal출발지 터미널 도착
linehaul간선 이동 중
at_destination_terminal도착지 터미널 도착
out_for_delivery배송출발
delivered배송완료
exception배송 이상(주소불명 등). 물건은 이미 택배사에 있습니다
returned반송. 인수 후 되돌아간 것입니다
cancelled취소. 인수 전 취소인지 후인지 구분되지 않습니다판단 보류
unknown문구를 단계로 옮기지 못함판단 보류

accepted만 있고 그 뒤가 없는 상태가 며칠째 그대로면 가송장을 의심할 신호입니다. 다만 접수 당일·주말·연휴에는 정상 건도 그렇게 보이므로 경과 시간을 함께 보세요.

번호 자체가 없는 경우

가장 강한 신호는 404 tracking_number_not_found입니다. 택배사 전산에 그 번호가 아예 없다는 뜻이라, 만들어 낸 번호이거나 다른 택배사 번호를 잘못 등록한 경우입니다. 422 carrier_not_detected(자동 감지 실패)와는 다릅니다. 그건 우리가 택배사를 못 고른 것이니 carrier를 지정해 다시 물어야 합니다.

월 1만 건 한도 안에서 조회하기

  1. events=latest로 가볍게 조회합니다. 다만 최신 이벤트 하나만 오므로 이미 지나간 picked_up이 안 보일 수 있습니다. 인수 여부를 처음 확정할 때는 events=full로 한 번 받고, 확정된 뒤의 추적에만 latest를 쓰세요.
  2. If-None-Match로 조건부 요청을 보냅니다. 내용이 그대로면 304가 오고 쿼터가 차감되지 않습니다. 하루 수천 건을 주기적으로 조회하는 서비스에서는 이 방법이 쿼터를 가장 많이 아껴줍니다. (캐시와 폴링)
  3. 인수가 확인되면 그 건은 목록에서 뺍니다. 목적이 "인수 여부"이므로 status.is_final까지 갈 필요가 없습니다.
  4. 429·503Retry-After를 지켜 재시도합니다. 502 carrier_error쿼터를 차감하지 않으므로 바로 재시도해도 됩니다.
인수 여부 판정
# "이미 인수됨"을 뜻하는 단계를 전부 나열하지 않는다. 그러면 나중에
# 택배사가 새 단계를 추가할 때마다 이 목록도 같이 고쳐야 하고, 고치는 걸
# 잊으면 이미 인수된 건이 "아직 접수만 됨"으로 잘못 판정된다. 대신 "아직
# 인수 전"과 "판단 불가"만 좁게 정의하고, 그 밖의 모든 단계는 인수된 것으로
# 본다. 모르는 새 단계가 와도 안전한 쪽(인수됨)으로 기본 처리된다.
NOT_YET_STAGES = {"accepted"}          # 접수만 되고 아직 인수 전
UNCLEAR_STAGES = {"cancelled", "unknown"}  # 인수 전/후를 구분할 수 없음


def pickup_state(body):
    """('picked_up' | 'not_yet' | 'unclear', 집화 시각 or None)"""
    # status.code가 아니라 이력 전체를 본다. 이미 간선으로 넘어간 건은
    # 지금 상태가 linehaul이라 picked_up과 절대 같지 않다.
    events = body.get("events", [])
    stages = {e.get("stage") for e in events}
    if not stages:
        return "unclear", None
    if stages <= NOT_YET_STAGES:
        return "not_yet", None
    if stages <= NOT_YET_STAGES | UNCLEAR_STAGES:
        return "unclear", None

    for event in events:
        if event.get("stage") not in NOT_YET_STAGES | UNCLEAR_STAGES:
            # date_only면 시각이 없다. 00:00으로 읽지 말 것.
            at = event["occurred_at"] if event.get("time_precision") == "exact" else None
            return "picked_up", at
    return "unclear", None

이 판정은 "택배사 전산에 인수 기록이 있다"까지만 말해 줍니다. 빈 상자를 접수한 경우처럼 전산상 정상이지만 실물이 다른 건은 이 API로 알 수 없습니다. 가송장 판정의 한 축으로 쓰고, 최종 판단은 다른 근거와 함께 하세요.

고객에게 실시간 위치 공유하기

별도의 위치추적 화면을 만들지 않아도 됩니다. 택배사 코드와 운송장번호로 공유 링크를 만들어 고객에게 보내면 실시간 위치추적 페이지가 바로 열립니다.

카카오톡처럼 링크 미리보기를 지원하는 곳에서는 현재 배송 상태가 아래와 같은 카드로 표시됩니다. 실제 카드의 상태와 위치는 링크를 미리보는 시점의 조회 결과에 따라 달라집니다.

합성 운송장번호 12345678901이 배송 출발 상태인 택배넷 실시간 위치추적 링크 미리보기 예시
메신저에 표시되는 링크 미리보기 예시 문서용 합성번호와 예시 상태를 사용했습니다.

공유 링크 만들기

완성된 링크
https://taekbae.net/live?courier=cj&tracking_number=12345678901
JavaScript 예시
function buildLiveTrackingUrl(courier, trackingNumber) {
  const url = new URL("https://taekbae.net/live");
  url.searchParams.set("courier", courier);
  url.searchParams.set("tracking_number", trackingNumber);
  return url.toString();
}

const shareUrl = buildLiveTrackingUrl("cj", "12345678901");

couriertracking_number를 모두 넣으면 링크를 연 즉시 해당 운송장을 조회합니다. 값을 비우면 운송장번호를 직접 입력하는 조회 화면이 열립니다. courier에 넣을 값은 택배사 목록의 코드와 같습니다. URLSearchParams를 사용하면 택배사 코드와 운송장번호가 URL에 맞게 안전하게 인코딩됩니다.

카카오 알림톡으로 자동 발송하는 기능은 아직 지원하지 않습니다. 앞으로 지원할 계획입니다. 지금은 위 주소를 문자·카카오톡 등으로 직접 보내는 방법만 됩니다.

배송 상태 계속 확인하기

한 번 조회하고 끝나는 게 아니라, 배송이 끝날 때까지 같은 운송장번호를 주기적으로 다시 물어봐야 하는 경우입니다. 이때만 아래 세 가지를 신경 쓰면 됩니다.

폴링은 status.is_final로 멈추세요. 문자열을 "배송완료"와 비교하는 코드는 반송·취소 건에서 영원히 돕니다. 그리고 매 요청에 ETag를 실으면 변화가 없을 때 304가 오고 쿼터가 차감되지 않습니다. 자세한 사용법은 캐시와 폴링을 보세요.

304404를 같은 값으로 뭉개지 마세요. 304는 "가진 결과가 아직 유효하다", 404는 "택배사에 그 운송장이 없다"입니다. 둘을 함께 null로 돌려주면 존재하지 않는 운송장이 화면에 '아직 변화 없음'으로 뜨고, 404차감 대상이라 폴링이 멈추지 않은 채 한도만 깎입니다.

이 세 가지를 실제로 구현한 코드는 코드로 붙이기의 각 언어 예제에 이미 들어 있습니다. track() 함수가 is_final·ETag·404를 전부 처리하니, 폴링 루프에서 그 함수를 그대로 반복 호출하면 됩니다.

배송 조회

GET /v1/tracking/{tracking_number}

파라미터

이름위치필수설명
tracking_number경로필수 영숫자 8~20자. 하이픈은 자동 제거하고 영문은 대문자로 정규화합니다.
carrier쿼리선택 택배사 코드. 생략하면 자동 감지합니다. 빈 값(carrier=)은 생략과 같게 처리합니다.
events쿼리선택 full(기본) · latest · none. 폴링에는 latest가 가볍습니다. 대소문자는 무시합니다.
eta BETA쿼리선택 도착 예측 포함 여부. 기본 false(미포함)이라 eta 키가 아예 오지 않습니다. 켜려면 eta=true. 1/0, yes/no, on/off도 받습니다.

각 파라미터는 한 번만 보내세요. 같은 파라미터를 두 번 보내거나 위 목록으로 해석할 수 없는 값을 보내면 400 invalid_parameter로 거절하고, 응답의 parameter가 어느 파라미터인지 알려줍니다. 조용히 기본값으로 처리하지 않습니다.

응답 필드

값이 없는 필드는 null이 아니라 키 자체가 빠집니다. 아래 항상 표시가 없는 필드는 없을 수 있다고 보고 짜세요.

최상위

필드타입설명
tracking_numberstring정규화된 운송장번호. 하이픈을 제거하고 영문을 대문자로 올린 값이라 보낸 값과 다를 수 있습니다. 대조할 때는 이 값을 쓰세요. 항상
carrierobject어느 택배사로 조회했는지. 항상
statusobject지금 배송 상태를 정규화한 값. 항상
last_event_atstring가장 최근 이벤트의 발생 시각(RFC 3339). 이벤트가 하나도 없으면 빠집니다. 변화 감지에 쓰기 좋은 값입니다.
eventsarray배송 이력. events=none이어도 키는 항상 있고 빈 배열이 옵니다. 최신순이 아니라 발생 순(오래된 것부터)입니다. 항상
partiesobject수하인·송하인. 주는 택배사에서만 옵니다.
delivery_agentobject배송 담당자. 주는 택배사에서만 옵니다. 지원 표
eta BETAobject도착 예측. ?eta=true로 요청했을 때만 옵니다.
as_ofstring택배사에서 정보를 가져온 시각. 응답을 만든 시각이 아니라 데이터의 나이입니다. 캐시된 응답이면 과거일 수 있습니다. 항상

carrier

필드타입설명
codestring택배사 코드(cj, hanjin …). 다음 조회에 carrier로 그대로 넣으면 자동 감지를 건너뜁니다. 전체 목록 항상
namestring한국어 표시명. 분기 조건으로 쓰지 마세요. 문구는 바뀔 수 있고 code는 유지됩니다. 항상
detectedboolean true우리가 추론한 택배사입니다(carrier를 생략한 경우). 추론이 틀릴 수 있으니, 사용자에게 보여주는 화면이라면 "다른 택배사인가요?"를 열어 둘 근거가 됩니다. 운송장번호는 택배사마다 따로 발급되는 값이라, 서로 다른 택배사가 우연히 같은 번호를 각자 실제 배송 건에 쓰고 있는 경우가 드물게 있습니다. 그러면 추론이 가리킨 택배사도 진짜 데이터를 돌려주기 때문에 조회 자체는 성공한 것처럼 보이지만, 그게 사용자가 찾던 화물이 아닐 수 있습니다. "결과 없음"과는 다른, 더 조용한 실패입니다.
false면 요청에서 지정한 값을 그대로 돌려준 것입니다. 필드 자체가 빠지는 게 아니라 실제로 false 값이 옵니다. 항상
carrier.detected 두 경우 비교
// carrier를 생략해 자동 감지됐을 때
"carrier": { "code": "cj", "name": "CJ대한통운", "detected": true }

// ?carrier=cj 로 직접 지정했을 때 (필드가 빠지지 않고 그대로 false로 옵니다)
"carrier": { "code": "cj", "name": "CJ대한통운", "detected": false }

status

필드타입설명
codestring13개 택배사의 서로 다른 문구를 하나로 정규화한 값. 분기는 이 값으로 하세요. 전체 목록 항상
labelstring한국어 표시 문구. 화면용이며 바뀔 수 있습니다. 항상
is_finalboolean더 이상 변하지 않는 상태(delivered·returned·cancelled). 폴링 종료 조건으로 쓰세요. 이걸 안 보면 배송이 끝난 건을 영원히 다시 조회하게 됩니다. 항상

events[]

필드타입설명
stagestring이벤트 하나를 정규화한 단계. status.code같은 값 집합입니다. "언제 어느 단계를 지났나"를 찾을 때 보는 값입니다. 항상
status_textstring택배사가 준 원문 문구("간선상차", "집화처리" …). 정규화가 문구를 바꾸지 않습니다(화면용 공백·링크 문구·연락처만 걷어냅니다). 사람에게 보여줄 때 씁니다.
occurred_atstring발생 시각(RFC 3339, KST 오프셋). time_precision을 먼저 보세요.
occurred_at_rawstring택배사가 준 시각 문자열 원문. 우리 파싱을 의심할 때 대조용입니다.
time_precisionstringexact(날짜+시각) · date_only(날짜만) · missing(시각 없음). date_only인데 occurred_at을 그대로 믿으면 없는 시각을 00:00으로 취급하게 됩니다. 항상
locationstring취급 지점("대전HUB", "서울강남" …). 택배사 표기 그대로라 표준 코드가 아닙니다.
countrystringISO 3166-1 alpha-2. 국제특송에서만 채워집니다.

parties · delivery_agent

필드타입설명
parties.receiver.namestring수하인. 택배사가 이미 가린 값("홍*동")을 그대로 전달합니다.
parties.receiver.maskedboolean위 값이 가려진 것인지. true면 전체 이름이 아니므로 대조 키로 쓰면 안 됩니다.
parties.sender.*object송하인. 구조는 receiver와 같습니다.
delivery_agent.namestring배송 담당 기사명.
delivery_agent.phonestring담당 기사 연락처. 주는 택배사에서만 옵니다.

eta BETA ?eta=true 일 때만

필드타입설명
statusstringavailable이면 예측이 나왔다는 뜻입니다. 그 외에는 아래 시각 필드가 비어 있을 수 있습니다.
estimated_delivery_fromstring예상 도착 구간의 시작. 단일 시각이 아니라 구간으로 옵니다.
estimated_delivery_tostring예상 도착 구간의 . 고객 안내에 하나만 쓸 거라면 이쪽(늦은 쪽)이 안전합니다.
p50_remaining_minutesinteger남은 시간의 중앙값(분). 절반은 이보다 빨리, 절반은 늦게 도착했다는 뜻입니다.
p90_remaining_minutesinteger남은 시간의 90분위(분). "이 시간까지는 대체로 온다"에 가깝습니다.
confidencestringhigh · medium · low. low면 고객에게 그대로 약속하지 마세요.
support_countinteger이 예측의 근거가 된 과거 배송 건수. 작을수록 흔들립니다.
fallback_levelstring같은 구간 표본이 없어 더 성긴 사전분포로 물러났을 때만 채워집니다. 이 키가 있으면 그만큼 덜 구체적인 근거라는 신호입니다.
model_versionstring예측 모델 버전. 예측이 이상할 때 문의에 함께 적어 주세요. eta가 오면 항상 함께 옵니다.
as_ofstring이 예측을 계산한 시각. eta가 오면 항상 함께 옵니다.

time_precision을 확인하세요. 택배사는 시각 없이 날짜만 주거나 "2026-07-16 --:--" 같은 자리표시자를 줍니다. 이 값이 date_only인데 occurred_at을 그대로 믿으면 없는 시각을 00:00으로 취급하게 됩니다.

값이 없는 필드는 null이 아니라 키 자체가 빠집니다. "이 택배사는 지원하지 않음"과 "이번 건에 값이 없음"은 택배사 지원 표로 구분하세요.

배송 상태

13개 택배사의 서로 다른 문구를 하나의 값 집합으로 정규화합니다. 택배사별 분기를 짤 필요가 없습니다.

codelabel종결
accepted 접수 진행 중
picked_up 집화완료 진행 중
at_origin_terminal 출발지 터미널 진행 중
linehaul 간선 이동 중 진행 중
at_destination_terminal 도착지 터미널 진행 중
out_for_delivery 배송출발 진행 중
delivered 배송완료 종결
exception 배송 이상 진행 중
returned 반송 종결
cancelled 취소 종결
unknown 확인 불가 진행 중

is_finaltrue인 상태에 도달하면 폴링을 멈추세요. 배송완료만 종결이 아닙니다. 반송·취소도 더 이상 변하지 않습니다.

택배사와 지원 필드

GET /v1/carriers

조회하려고 먼저 불러야 하는 엔드포인트가 아닙니다. 자동 감지가 있으니 코드를 몰라도 조회됩니다. 이 목록은 "이 택배사에서 왜 이 필드가 안 오는가"에 답하기 위한 것이고, 쿼터를 차감하지 않습니다.

응답 필드

택배사마다 code·name·auto_detect·supports 네 개가 옵니다. 지원 여부는 불리언 필드가 아니라 supports 배열의 원소로 표현합니다. carrier.delivery_agent 같은 키는 없습니다.

응답 200
{
  "carriers": [
    { "code": "cj",  "name": "CJ대한통운", "auto_detect": "reliable", "supports": ["events", "eta"] },
    { "code": "slx", "name": "SLX택배",    "auto_detect": "weak",     "supports": ["events", "eta", "delivery_agent"] }
  ]
}
배송 담당자를 주는 택배사인지 확인하기
const byCode = Object.fromEntries(
  (await res.json()).carriers.map((c) => [c.code, c])
);
byCode["slx"].supports.includes("delivery_agent");  // true
code택배사자동 감지supports
cj CJ대한통운 신뢰 events, eta
hanjin 한진택배 신뢰 events, eta
lotte 롯데택배 신뢰 events, eta
logen 로젠택배 표본 부족 events, eta
epost 우체국택배 신뢰 events, eta
kyungdong 경동택배 표본 부족 events, eta
daesin 대신택배 표본 부족 events, eta
slx SLX택배 표본 부족 events, eta, delivery_agent
gs25 GS25반값택배 표본 부족 events, eta
cu CU편의점택배 미지원 events, eta
aliexpress 알리익스프레스 접두어 events, eta
dhl DHL 미지원 events, eta
ems 우체국 EMS 접두어 events, eta

표본 부족은 자동 감지 대상이지만 학습 데이터가 적어 1순위를 못 맞힐 수 있다는 뜻입니다. 이 택배사들은 carrier를 지정하는 편이 확실합니다. supportsdelivery_agent가 없는 택배사는 원본 응답에 배송 담당자 항목 자체가 없어서, 조회 응답에도 그 키가 나오지 않습니다.

사용량

GET /v1/usage

남은 호출 수와 리셋 시각을 돌려줍니다. 헤더는 요청을 보내야만 볼 수 있어서 대시보드나 알림을 만들기 어렵기 때문에 별도로 둡니다. 이 호출은 쿼터를 차감하지 않습니다.

{
  "enforced": true,
  "remaining": 431,
  "reset_at": "2026-07-27T00:00:00+09:00",
  "scope": "daily"
}

캐시와 폴링

모든 조회 응답에 ETagCache-Control이 붙습니다. If-None-Match로 조건부 요청을 보내면 내용이 그대로일 때 304가 돌아옵니다.

# 1) 먼저 조회하고 응답 헤더에서 ETag를 받는다 (-i로 상태줄·헤더까지 출력)
curl -i "https://taekbae.net/v1/tracking/$TRACKING_NUMBER" \
  -H "Authorization: Bearer $TAEKBAE_API_KEY"

HTTP/1.1 200 OK
etag: "c657463a16ec1a5e"
cache-control: max-age=300

# 2) 받은 ETag를 그대로 실어 다시 부른다. 내용이 그대로면 304
curl -i "https://taekbae.net/v1/tracking/$TRACKING_NUMBER" \
  -H "Authorization: Bearer $TAEKBAE_API_KEY" \
  -H 'If-None-Match: "c657463a16ec1a5e"'

HTTP/1.1 304 Not Modified

ETag 값은 본문 내용마다 다릅니다. 위 값을 그대로 쓰지 말고 1)에서 받은 값을 실으세요. -i가 없으면 304는 본문이 없어 화면에 아무것도 찍히지 않아, 조건부 요청이 동작한 것인지 요청이 실패한 것인지 구분할 수 없습니다.

택배사를 실제로 부를 때만 호출 수를 셉니다. 캐시에서 답이 나가면 200이든 304든 차감하지 않습니다. 조건부 요청을 쓰는 쪽이 언제나 이득입니다.

Cache-Controlmax-age가 권장 폴링 주기입니다. 배송이 끝난 건은 24시간, 배송출발 상태는 5분입니다. status.is_finaltrue면 폴링을 멈추는 게 가장 좋습니다.

호출 한도

응답 헤더로 잔량을 알려줍니다.

헤더의미
RateLimit-Remaining현재 창에서 남은 호출 수
RateLimit-Reset창이 리셋될 때까지 남은 초
RateLimit-Scopedaily 또는 monthly. 두 창 중 더 임박한 쪽을 알려줍니다.

성공 응답뿐 아니라 쿼터를 조회한 오류 응답에도 같은 헤더가 옵니다. 차감이 일어나는 404 tracking_number_not_found도 포함이라, 접수 전 번호를 폴링하는 동안에도 잔량을 헤더만으로 추적할 수 있습니다. 인증 실패(401)처럼 쿼터를 보기 전에 끊긴 응답에는 없습니다.

한도를 넘기면 429Retry-After가 옵니다. 이때도 잔량 헤더가 함께 옵니다. 어느 창에 걸렸는지는 응답의 scope로 확인하세요.

호출이 실패해도 우리 잘못이면 차감하지 않습니다. 택배사 장애(503)나 서버 오류(5xx)는 자동으로 되돌립니다. 아래 표의 차감 열을 참고하세요.

오류

오류는 항상 4xx/5xx로 옵니다. 200 본문에 실패를 담지 않습니다. 본문은 RFC 9457 application/problem+json 형식입니다.

{
  "type": "https://taekbae.net/docs/api/errors/carrier_unavailable",
  "title": "택배사 시스템에 접근할 수 없습니다",
  "status": 503,
  "code": "carrier_unavailable",
  "carrier": "cj",
  "detected": true,
  "request_id": "req_75999e533cfc499a995c0bf769579d7e"
}

code로 분기하세요. title 문구는 바뀔 수 있지만 code는 유지됩니다. 문의할 때는 request_id를 함께 알려주시면 해당 요청을 바로 찾을 수 있습니다.

필드설명
code안정된 기계 판독용 코드. 분기는 이 값으로 하세요.
carrier이 오류가 난 택배사 코드. 택배사를 실제로 부른 오류에만 옵니다.
detectedcarrier우리가 추론한 값인지(carrier를 생략한 호출인지). /v1/tracking의 오류에 실립니다. 200carrier.detected와 같은 값입니다.
candidates · triedcarrier_not_detected에만. 자동 감지 참고.
scopequota_exceeded에만. daily 또는 monthly.

detectedtrue5xx는 택배사 장애가 아닐 수 있습니다. 우리 추론이 빗나가 엉뚱한 택배사를 부른 것일 수 있고, 그렇다면 몇 번을 재시도해도 결과가 같습니다. 이때는 재시도 대신 carrier를 직접 지정해 한 번 더 확인해 보세요.

invalid_tracking_number 400 차감 없음

운송장번호 형식이 아닙니다. 영숫자 8~20자여야 하고 숫자가 최소 1개 필요합니다.

대응 입력값을 확인하세요. 하이픈은 우리가 제거하므로 그대로 보내도 됩니다.

invalid_parameter 400 차감 없음

쿼리 파라미터 값을 해석할 수 없거나 같은 파라미터를 두 번 보냈습니다. 응답의 `parameter`가 어느 파라미터인지 알려줍니다.

대응 `events`는 `full`·`latest`·`none`, `eta`는 `true`/`false`(`1`/`0`, `yes`/`no`, `on`/`off`도 됩니다)만 받습니다. 각 파라미터는 한 번만 보내세요.

missing_api_key 401 차감 없음

Authorization 헤더가 없거나 Bearer 형식이 아닙니다.

대응 `Authorization: Bearer tb_...`를 붙이세요. 쿼리스트링 키는 지원하지 않습니다.

invalid_api_key 401 차감 없음

존재하지 않거나 폐기된 키입니다.

대응 내 계정에서 키를 다시 확인하거나 새로 발급하세요.

carrier_not_supported 404 차감 없음

지원하지 않는 택배사 슬러그입니다. 응답의 `supported`에 전체 목록이 들어 있습니다.

대응 `GET /v1/carriers`로 사용 가능한 코드를 확인하세요.

tracking_number_not_found 404 차감

택배사에 해당 운송장이 없습니다. 접수 직후라 아직 등록되지 않았을 수 있습니다.

대응 접수 직후라면 잠시 뒤 재시도하세요. 택배사 장애와는 구분되는 응답입니다.

not_found 404 차감 없음

그런 엔드포인트가 없습니다. 운송장이 없다는 뜻이 아닙니다. 그건 `tracking_number_not_found`입니다.

대응 `GET /v1/openapi.json`의 경로 목록과 대조하세요. 오타난 경로가 대부분입니다.

method_not_allowed 405 차감 없음

경로는 있지만 그 메서드를 지원하지 않습니다. `Allow` 헤더에 허용 메서드가 옵니다.

대응 `Allow` 헤더의 메서드로 다시 호출하세요. v1은 전부 조회 API라 `GET`입니다.

carrier_not_detected 422 차감

운송장번호만으로 택배사를 특정하지 못했습니다. `tried`가 이미 조회한 택배사, `candidates`가 아직 시도하지 않은 후보입니다. 후보를 다 썼거나 애초에 못 뽑았으면 `candidates`는 빈 배열입니다. 택배사를 실제로 조회했으므로 1건 차감합니다. 후보를 하나도 못 뽑아 상류를 아예 부르지 않은 경우(`tried`가 빈 배열)만 차감하지 않습니다.

대응 `candidates`가 비어 있지 않으면 그중 하나를 `carrier`로 지정해 다시 호출하세요. 비어 있으면 `GET /v1/carriers`에서 택배사를 직접 고르세요. `tried`의 택배사는 방금 조회해 정보가 없었으므로 다시 시도해도 결과가 같습니다.

quota_exceeded 429 차감 없음

요청 한도를 초과했습니다. `scope`가 일·월 중 어느 창인지 알려줍니다.

대응 `Retry-After`만큼 기다렸다 재시도하세요.

carrier_error 502 차감 없음

택배사가 응답했지만 우리가 해석하지 못했습니다.

대응 재시도하고, 반복되면 `request_id`와 함께 문의하세요.

carrier_unavailable 503 차감 없음

택배사 시스템에 접근할 수 없습니다. 택배사 점검·장애·차단입니다.

대응 `Retry-After` 후 재시도하세요. 운송장이 없는 것과는 다릅니다.

internal_error 500 차감 없음

우리 쪽 오류입니다.

대응 `request_id`와 함께 문의해 주세요.

tracking_number_not_foundcarrier_unavailable은 다릅니다. 전자는 택배사에 그 운송장이 없는 것(접수 직후일 수 있음), 후자는 택배사 시스템에 접근 자체가 안 되는 것입니다. 이 둘을 구분해야 무한 재시도와 잘못된 안내를 피할 수 있습니다.

OpenAPI 스펙

GET /v1/openapi.json 내려받기

OpenAPI 3.1 스펙을 그대로 제공합니다. 인증 없이 받을 수 있으니 키를 발급받기 전에 SDK부터 만들어 봐도 됩니다.

npx @openapitools/openapi-generator-cli generate \
  -i https://taekbae.net/v1/openapi.json \
  -g typescript-fetch -o ./taekbae-sdk

필드 추가는 하위호환 변경으로 봅니다. 클라이언트는 모르는 필드를 무시하도록 만들어 주세요. 필드 제거나 의미 변경은 /v2로 가며, 폐기 예정은 Deprecation·Sunset 헤더로 미리 알립니다.