Partner Data API

개발자 문서

파트너 데이터 API 는 통합 파트너가 우리 분석 데이터를 프로그래밍으로 가져가 자체 어드민·제품에 렌더하는 독립 상품입니다. 웹 대시보드가 아니며, 어떤 웹 구독으로도 열리지 않습니다. 베이스 경로는 POST /partner/v1/… — API 는 POST 전용입니다.

두 개의 별개 축

축1 — SaaS 대시보드 티어: 사람이 우리 UI 에 로그인, per-tenant 월정액, JWT 인증. 축2 — API 토큰(이 문서): 기계가 우리 데이터를 HTTP 로 가져감, 테넌트리스 크리덴셜, 호출당 토큰 미터링.

웹 구독은 API 토큰 0개를, API 크리덴셜은 대시보드 접근 0을 포함합니다 — 두 축은 서로 교차하지 않습니다. (ChatGPT Plus 웹 vs OpenAI API 관계.)

크리덴셜의 구조(테넌트리스) · 헤더 처리 · 필수 siteId 는 「인증」 절에 있습니다.

Get access

접근 — 크리덴셜 발급

준비 중· 셀프 발급 개발자 콘솔

파트너는 크리덴셜을 셀프서비스로 발급하지 않습니다. 현재는 Statlane 플랫폼 팀이 클라이언트와 grant(테넌트 × 스코프) · 월 쿼터를 발급합니다. 셀프 발급 개발자 콘솔은 준비 중이며, 지금 발급받는 경로는 문의 하나입니다.

  • 발급 응답에서 한 번만 노출되는 값은 클라이언트 UUID · 서명키(psk_…) · 시크릿(plk_…) 세 가지입니다. 다시 조회할 수 없으니 즉시 시크릿 매니저에 보관하십시오.
  • 신규 크리덴셜의 기본 인증 모드는 서명(PSR1) 입니다. 서명 통합 일정이 필요하시면 발급 시점에 말씀해 주시면 시크릿 모드 또는 병존 모드로 내어 드립니다.
  • 시크릿은 서명 모드로 발급되더라도 함께 드립니다 — 문제가 생겼을 때 되돌릴 수단이 그 시점에 이미 손에 있어야 하기 때문입니다.
  • 만료일(expires_at)은 발급 시 필수이며 실제로 집행됩니다. 만료된 크리덴셜은 401 이고, 잘못된 시크릿과 구분되지 않습니다.
  • 크리덴셜은 자동 갱신되지 않습니다. 시크릿이 1회만 노출되므로 서버가 스스로 회전하면 새 값을 전달할 방법이 없기 때문입니다 — 갱신은 담당자와 합의해 진행하는 절차입니다.

찾는 것이 개인 API 키(축1)라면 — 로그인 사용자가 자기 대시보드 데이터를 스크립트로 읽는 용도의 X-API-Key 는 콘솔에서 바로 발급·폐기할 수 있습니다. 이 키는 파트너 데이터 API 를 인증하지 않으며, 구독 티어 게이트도 우회하지 않습니다. 개인 API 키 콘솔 →

siteId

siteId 는 어디서 얻는가

모든 요청 본문의 첫 필수 필드가 siteId (UUID) 입니다. 누락·무효면 400 PT005 PARTNER_SITE_REQUIRED 입니다. 실경로는 둘뿐입니다.

  1. ① grant 를 부여할 때 우리가 함께 드립니다. grant 는 테넌트 단위(client × tenant × scope)라 그 테넌트의 사이트 전부를 덮습니다 — 부여 시점에 대상 사이트 UUID 목록을 전달해 드리고, 고객이 나중에 사이트를 추가해도 새 grant 는 필요 없습니다(추가된 사이트도 같은 grant 로 덮입니다).
  2. ② 고객(테넌트)이 자기 콘솔에서 확인해 전달합니다. 사이트 목록은 고객 본인의 대시보드 세션으로 조회할 수 있습니다(축1의 GET /me/sites 가 siteId 를 돌려줍니다). 이 경로는 고객이 실행하는 것이며, 파트너 크리덴셜로는 그 API 를 부를 수 없습니다.
  3. ③ POST /partner/v1/sites 로 직접 조회합니다(권장). 이 크리덴셜에 부여된 사이트를 siteId·이름·사용 가능한 scope 와 함께 돌려줍니다. 요청 본문이 없고 과금되지 않습니다(cost_units 0). 고객이 사이트를 추가하면 다음 호출부터 목록에 나타나므로, 설정에 UUID 를 하드코딩하지 않아도 됩니다.
Authentication

인증 — 두 스킴과 기본값

스킴은 둘이고, 어느 쪽을 쓰는지는 크리덴셜 한 건 단위로 정해집니다. 아래 모드표에서 내 크리덴셜의 자리를 먼저 찾으십시오. 서명값을 어떻게 만드는지는 「요청 서명 PSR1 — 심화」 절에 있습니다.

인증 모드 3종#

auth_mode누구에게서명 헤더를 보내면안 보내면
secretDDL DEFAULT — 기존에 발급된 크리덴셜(오늘 동작 보존)서명 헤더를 무시하고 시크릿을 검증합니다시크릿 검증
either전환기(운영자가 크리덴셜 한 건 단위로 설정)서명만 봅니다 — 서명이 틀리면 시크릿이 맞아도 401 입니다(조용한 다운그레이드 차단)시크릿 검증 + 응답에 Deprecation / Sunset
signature신규 발급 기본값서명만 검증401

서명 헤더 — 신규 발급 기본값#

헤더필수값
X-Partner-Client필수클라이언트 UUID — 시크릿 스킴과 동일한 공개 식별자(전환해도 바뀌지 않음)
X-Partner-Timestamp필수epoch 초 (UTC 정수). ISO8601 금지 — 허용 오차 ±300초
X-Partner-Nonce필수요청마다 새 난수, 소문자 hex 정확히 32자 (= 16바이트)
X-Partner-Signature필수"v1=" + base64(HMAC-SHA256(서명키, 정규화문자열))
Idempotency-Key선택과금 중복 방지 키 — 같은 키로 재시도해도 1회만 과금됩니다(응답은 매번 새로 계산). 요청 자체를 중복 제거하거나 이전 응답을 재생하지는 않습니다. 서버가 클라이언트 단위로 스코프하며(타 클라이언트 키와 충돌 불가) 길이 제한은 없습니다. ★보내면 서명 대상 7번째 줄에 들어갑니다
Content-Type필수application/json

X-Partner-Secret 은 서명 모드에서 보내지 않습니다 — 서명키는 네트워크에 흐르지 않습니다. 요청 본문은 256KB(262,144 바이트) 이하여야 합니다. 이 상한은 인증보다 앞에서 적용되며(미인증 호출자가 우리 힙에 임의 크기를 할당시키지 못하게), 초과분은 별도 코드가 아니라 401 로 떨어집니다. 서명 검증이 본문 전체를 붙들어야 하므로 이 상한은 부수적 하드닝이 아니라 스킴의 선행조건입니다.

legacy 공유 시크릿 헤더 — 기존 파트너용#

공유 시크릿은 legacy 스킴입니다. 클라이언트가 TLS 위로 시크릿을 직접 보내면 서버는 해시(SHA-256 + pepper)해서 상수시간 비교합니다. 요청마다 계산하는 값이 없어 구현이 가장 단순한 대신, 시크릿이 매 요청 네트워크를 흐릅니다 — 프록시 로그·APM 헤더 덤프·지원 티켓에 붙은 HAR 한 건이 만료일까지 유효한 전권 크리덴셜이 됩니다. 그래서 요청 서명(PSR1)이 후속 스킴이며, 신규 발급은 기본이 서명입니다. 이미 시크릿으로 통합하신 파트너는 아무것도 바꾸지 않아도 됩니다 — 전환은 크리덴셜 단위로 합의해 진행하며, 기존 방식이 예고 없이 끊기지 않습니다.

헤더필수값
X-Partner-Client필수프로비저닝 시 발급된 클라이언트 UUID
X-Partner-Secret필수원시 시크릿, 형식 plk_ + 48 hex
Idempotency-Key선택과금 중복 방지 키 — 같은 키로 재시도해도 1회만 과금됩니다(응답은 매번 새로 계산). 요청 자체를 중복 제거하거나 이전 응답을 재생하지는 않습니다. 서버가 클라이언트 단위로 스코프하며(타 클라이언트 키와 충돌 불가) 길이 제한은 없습니다
Content-Type필수application/json
legacy 시크릿 스킴 요청bash
curl -X POST https://api.statlane.kr/partner/v1/conversions/summary \
  -H "X-Partner-Client: <CLIENT_UUID>" \
  -H "X-Partner-Secret: <PLK_SECRET>" \
  -H "Idempotency-Key: 2026-08-15-summary-01" \
  -H "Content-Type: application/json" \
  --data-raw '{"siteId":"<SITE_ID>","from":"2026-07-01","to":"2026-07-31"}'
  • 크리덴셜(PartnerClient)에는 tenant_key 컬럼이 없습니다 — 마스터 키가 될 수 없습니다. 테넌트 귀속은 오직 인가 엣지(PartnerGrant = client × tenant × scope)를 통해서만 이뤄지며, grant 는 그 테넌트의 모든 사이트(현재·미래)를 덮습니다.
  • Authorization · X-API-Key 헤더는 다운스트림 도달 전 제거됩니다 — 파트너 인증 호출이 JWT·개인 토큰 필터를 건드릴 수 없습니다. (X-API-Key 는 별개 축 = 로그인 사용자 개인 토큰으로, 이 API 를 인증하지 않습니다.)
  • 모든 요청 본문은 siteId (UUID) 를 반드시 포함해야 합니다 — grant 핀 키입니다. 누락되거나 UUID 로 읽히지 않으면 400 PARTNER_SITE_REQUIRED 입니다.
Quickstart

첫 호출 — 60초

필요한 재료는 넷입니다. 전부 손에 있으면 아래를 그대로 실행할 수 있습니다.

  1. 베이스 URL — https://api.statlane.kr
  2. 환경변수 2개 — STATLANE_CLIENT_ID (클라이언트 UUID) · STATLANE_SIGNING_KEY (psk_… 문자열 그대로 — base64/hex 디코딩 금지)
  3. siteId — 프로그래밍으로 발견할 수 없습니다. 위 절의 두 경로 중 하나로 받아 설정에 보관하십시오.
  4. 경로 — 아래 엔드포인트 레퍼런스에서 고릅니다. 전부 POST 입니다.
서명(PSR1) 스킴 — 실행 가능한 전체 예시bash
#!/usr/bin/env bash
# bash + openssl · 의존성 0. 골든 벡터를 그대로 재현하려면 TS·NONCE 를 고정값으로 두면 된다.
set -euo pipefail

CLIENT_ID="$STATLANE_CLIENT_ID"
SIGNING_KEY="$STATLANE_SIGNING_KEY"          # psk_… 그대로. 디코딩 금지
API_PATH="/partner/v1/keyword-budget/summary"
BODY='{"siteId":"<SITE_ID>","from":"2026-08-01","to":"2026-08-16"}'

TS=$(date +%s)                                # epoch 초
NONCE=$(openssl rand -hex 16)                 # hex 32자
IDEM="bk-2026-08-17-0042"                     # 안 쓰면 IDEM=""  → 7번째 줄이 빈 줄로 남는다

DIGEST=$(printf '%s' "$BODY" | openssl dgst -sha256 | awk '{print $NF}')
CANONICAL=$(printf 'v1\nPOST\n%s\n%s\n%s\n%s\n%s\n%s' \
  "$API_PATH" "$CLIENT_ID" "$TS" "$NONCE" "$IDEM" "$DIGEST")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$SIGNING_KEY" -binary | base64)

# ★서명한 그 바이트를 그대로 보낸다(--data-raw, 재포맷 금지)
RESP=$(curl -sS -X POST "https://api.statlane.kr$API_PATH" \
  -H "Content-Type: application/json" \
  -H "X-Partner-Client: $CLIENT_ID" \
  -H "X-Partner-Timestamp: $TS" \
  -H "X-Partner-Nonce: $NONCE" \
  -H "X-Partner-Signature: v1=$SIG" \
  -H "Idempotency-Key: $IDEM" \
  -D /tmp/statlane.headers \
  --data-raw "$BODY")

# ★응답 envelope 은 { code, message, data } 다. 도메인 실패도 HTTP 200 으로 오므로
#   HTTP 상태가 아니라 code 를 봐야 한다("200" 이 아니면 실패).
CODE=$(printf '%s' "$RESP" | tr -d '[:space:]' | grep -o '"code":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
if [ "$CODE" != "200" ]; then
  echo "실패 — envelope code=[$CODE]" >&2
  echo "$RESP" >&2
  # 401 이면 시계부터: X-Partner-Server-Time(전 응답에 실림) vs 내 시각.
  grep -i '^x-partner-server-time:' /tmp/statlane.headers >&2 || true
  date +%s >&2
  exit 1
fi
echo "$RESP"

Node.js · Python · Java 구현은 「구현 샘플」에 같은 형태로 있습니다. 서버에 붙기 전에 골든 테스트 벡터로 자기 구현부터 오프라인 검증하시면 401 디버깅의 대부분이 사라집니다.

첫 통합에서 가장 자주 놓치는 것

성공 판정을 HTTP 상태코드로 하지 마십시오. 인증·권한·쿼터 거부(401 · 403 · 402 · 503 · 400 PT005 · 429)는 인증 필터가 직접 내보내므로 실제 상태코드가 나가지만, 본문 검증 실패와 컨트롤러 도메인 실패는 HTTP 200 으로 나갑니다(전역 규약). 실패라는 사실은 envelope 의 code 필드에만 있습니다 — code 값이 "200" 인지 확인하십시오. 과금 확정도 HTTP 상태가 아니라 내부 도메인-성공 플래그로 구동됩니다.

Conventions

전역 규약

아래는 8개 엔드포인트 전부에 공통인 것만 모았습니다. 엔드포인트별 차이는 레퍼런스 각 블록에 있습니다.

응답 envelope 과 성공 판정#

전역 envelopejson
{
  "code": "200",
  "message": "Success",
  "data": { }
}

성공 판정을 HTTP 상태코드로 하지 마십시오. 인증·권한·쿼터 거부(401 · 403 · 402 · 503 · 400 PT005 · 429)는 인증 필터가 직접 내보내므로 실제 상태코드가 나가지만, 본문 검증 실패와 컨트롤러 도메인 실패는 HTTP 200 으로 나갑니다(전역 규약). 실패라는 사실은 envelope 의 code 필드에만 있습니다 — code 값이 "200" 인지 확인하십시오. 과금 확정도 HTTP 상태가 아니라 내부 도메인-성공 플래그로 구동됩니다.

날짜 의미론#

  • from · to 는 양끝 포함(inclusive) 입니다. 하루치를 보려면 from 과 to 를 같은 날짜로 주십시오.
  • ★타입이 엔드포인트마다 다릅니다. behavior/events 만 Instant(예: 2026-08-01T00:00:00Z)이고, 나머지 6개(behavior/summary 포함)는 date(YYYY-MM-DD)입니다. market/trends 만 from/to 가 없고 lookbackDays 로 구간을 지정합니다.
  • 기간 캡도 엔드포인트마다 다릅니다: audience-fit · conversions/summary · ads · keyword-budget ≤ 180일, conversions/funnel ≤ 92일, behavior(events · summary) ≤ 31일, market lookbackDays 1–365. 캡 초과는 HTTP 200 + envelope code "400" 입니다.
  • 월 쿼터의 월 경계는 KST 기준 yyyyMM 입니다 — 요청의 from/to 타임존과는 무관합니다.

페이지네이션 — behavior/events 하나만#

8개 중 behavior/events 하나만 페이지네이션이 있습니다(keyset). 한 번의 호출은 최대 limit(≤200)건만 돌려주며, 응답의 hasMore 가 true 이면 nextCursor 를 다음 요청의 cursor 에 넣어 반복해야 합니다. nextCursor 가 null 이거나 hasMore 가 false 이면 마지막 페이지입니다. ★여기서 멈추면 에러 하나 없이 첫 페이지만 받고 전량을 받았다고 착각하게 됩니다 — 무증상 데이터 절단이라 눈으로 발견되지 않습니다. ★반복 호출은 매 호출마다 토큰이 차감됩니다(페이지 수 × cost_units).

keyset 반복json
// 1) 첫 페이지 — cursor 없이
{ "siteId": "<SITE_ID>", "from": "2026-08-01T00:00:00Z", "to": "2026-08-16T00:00:00Z", "limit": 200 }

// 2) 응답
{ "code": "200", "message": "Success",
  "data": { "events": [ /* 200건 */ ], "nextCursor": "eyJvIjoiMjAyNi0wOC0xNVQwNDoxMjozM1oifQ==", "hasMore": true } }

// 3) 다음 페이지 — 받은 nextCursor 를 cursor 로
{ "siteId": "<SITE_ID>", "from": "2026-08-01T00:00:00Z", "to": "2026-08-16T00:00:00Z", "limit": 200,
  "cursor": "eyJvIjoiMjAyNi0wOC0xNVQwNDoxMjozM1oifQ==" }

Idempotency-Key#

  • 재시도 = 새 타임스탬프 + 새 nonce + 재서명, Idempotency-Key 는 그대로 유지.
  • 같은 멱등키의 재시도는 과금 원장이 그 키로 묶여 1회만 과금됩니다(응답은 매번 새로 계산되며, 요청 자체를 중복 제거하거나 이전 응답을 재생하지는 않습니다). 키를 보내지 않으면 서버가 nonce 를 멱등키로 씁니다.
  • Idempotency-Key 에 길이 제한은 없습니다. 서버는 키를 그대로 저장하지 않고 전량을 해시해 클라이언트 단위로 스코프한 고정 길이 원장 PK 로 만듭니다. 그래서 귀사의 주문번호·트레이스 ID 체계를 그대로 쓰셔도 되고(접두가 길어도 무방합니다), 서로 다른 두 키가 앞부분이 같다는 이유로 한 건으로 접히는 일이 없습니다. 다른 클라이언트의 키와도 충돌하지 않습니다.

k-익명 봉인#

k-익명 봉인은 장애가 아니라 정상 응답입니다. 표본이 너무 적어 개인이 역으로 식별될 수 있는 구간에서는 해당 블록을 null 로 비우고 notice 로 사유를 밝힙니다. 임계는 순 구매자(uniq) 5명 · 퍼널 진입 세션 5개 · 요소별 uniq 5 이며, ads 는 반올림 후 전 지표가 0 이 되는 소액 채널을 행 단위로 제외합니다. ★null 을 0 으로 치환해 렌더하지 마십시오 — "매출 0원" 은 봉인과 전혀 다른 사실입니다.

정직성 라벨 6필드#

필드타입의미실제 값 예
estimatedboolean추정값 여부. false = 실측·관측 기반.false (현재 8개 엔드포인트 전부)
observationalboolean관측 실측 여부.true (현재 8개 엔드포인트 전부)
isRelativeIndexboolean상대지수 여부. true 면 절대량이 아닙니다 — 다른 대상과의 절대 비교나 금액 환산에 쓸 수 없습니다.market · audience_fit = true / behavior · conversions · ads · keyword_budget = false
methodVersionstring산출 방법의 버전. 필드 셰이프가 그대로여도 계산 방식이 바뀌면 이 값이 올라갑니다 — 시계열을 이어 붙일 때 이 값이 같은 구간끼리만 비교하십시오.'market.rising.v1' · 'audience_fit.summary.v1' · 'behavior.events.v1' · 'behavior.summary.v1' · 'conversions.summary.v1' · 'conversions.funnel.v1' · 'ads.summary.v1' · 'keyword_budget.summary.v1'
dataBasisstring이 숫자가 무엇에서 나왔는지. 근거가 다른 값을 한 화면에서 합산하지 않게 해 주는 필드입니다.'자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)' · '공개 플랫폼 데이터 실측(스크래핑 아님)'
caveatsstring[]한계·주의. 빈 배열일 수 있으나 null 은 될 수 없습니다. 상황에 따라 항목이 늘어납니다(예: 실집행 키워드 0건).['환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)']

표시 의무

★label 은 모든 응답에 non-null 로 관통합니다(소스 라벨이 비면 서버가 조립 단계에서 명시 실패합니다 — 라벨 없는 응답이 새지 않습니다). 이 값을 파싱해서 버리지 말고 화면에 함께 노출해 주십시오. 특히 isRelativeIndex=true 인 값을 절대량처럼 보여 주는 것, caveats 를 감춘 채 숫자만 크게 띄우는 것, null 을 0 으로 치환하는 것 — 이 셋은 우리가 봉인·라벨로 막아 둔 오해를 귀사 화면에서 되살립니다. 파트너 데이터 API 약관도 같은 취지입니다.

응답 헤더#

헤더언제 실리나값
X-Partner-Server-Time성공·실패 불문 전 응답서버 epoch 초. 401 이 사유를 알려주지 않으므로, 시계 오차를 스스로 진단할 수 있는 유일한 신호입니다.
Deprecation / Sunsetauth_mode='either' 에서 legacy 시크릿으로 인증됐을 때만RFC 8594. Deprecation: true 와(설정돼 있으면) 그 크리덴셜의 Sunset 시각. ★이건 인증 스킴 이야기이지 API 버전 이야기가 아닙니다.
Retry-After인증 실패 스로틀 429 에만초 단위 대기 시간. 이 값만큼 지나면 카운터가 소멸해 스스로 풀립니다(잠금이 아닙니다). 버스트 429 에는 붙지 않습니다.
X-Request-Id인증 통과 후 전 응답(200·402 포함)이 요청의 식별자. 문의하실 때 이 값을 알려주시면 해당 요청을 특정할 수 있습니다. Idempotency-Key 를 보내셨다면 그 키에서 파생되므로, 같은 키의 재시도는 같은 값을 받습니다.
X-Quota-Remaining월 과금·선불 모두. 서버가 잔량을 확정한 응답에만남은 호출 단위. 월 과금이면 상한−사용량, 선불이면 차감 후 잔액입니다. ★값을 확정하지 못한 경우 이 헤더는 아예 붙지 않습니다 — 0 으로 내려보내면 "다 썼다"로 오해되기 때문입니다. 헤더가 없으면 "이번 응답에서는 알 수 없음"으로 읽어 주세요.
X-Quota-Limit월 과금 계약에만이번 달 상한. 선불 계약에는 상한 개념이 없어 붙지 않습니다.
X-Quota-Reset월 과금 계약에만카운터가 새로 시작되는 시각(ISO-8601, KST 오프셋 포함). 월 키가 KST 기준이라 다음 달 1일 0시 KST 입니다. ★선불에는 붙지 않습니다 — 시간이 지나도 잔액은 차오르지 않고 충전이 필요하기 때문입니다.

위가 전부입니다. 402(월 한도 초과) 응답에도 X-Quota-Remaining · X-Quota-Reset 이 실리므로, 얼마나 남았고 언제 풀리는지는 응답만으로 알 수 있습니다. 다만 지난 달들의 사용 이력을 조회하는 API 는 아직 없습니다 — 과거 사용량이 필요하시면 담당자에게 문의해 주세요.

왜 조회인데 POST 인가#

  1. 요청 본문 전체가 서명 대상입니다(정규화 8번째 줄 = 본문 sha256). 조건을 쿼리스트링에 두면 프록시·APM 이 파라미터를 덧붙이는 순간 서명이 깨지므로, 서명 가능한 유일한 자리가 본문입니다.
  2. siteId · 기간 · steps 배열처럼 URL 길이 제한과 인코딩 사고에 취약한 조건이 필수입니다. steps 는 자유 입력 이벤트명 배열이라 쿼리스트링에 실으면 인코딩만으로도 장애가 납니다.
  3. 조건이 URL 에 남지 않으므로 프록시 로그·브라우저 히스토리·리퍼러에 조회 조건이 새지 않습니다.

★POST 전용은 부팅 시 검사됩니다. /partner/v1 하위에 비-POST 매핑이 추가되면 애플리케이션이 아예 뜨지 않습니다(PartnerV1MethodGuard). 런타임에서도 비-POST 는 403 이며, CORS 프리플라이트가 인증 실패로 둔갑하지 않도록 OPTIONS 만 예외입니다.

대신 파트너가 감수하는 것: HTTP 캐시(GET 기반 CDN·브라우저 캐시)를 쓸 수 없고, 링크 하나로 재현되는 요청을 만들 수 없으며, 일부 HTTP 클라이언트의 "GET 만 재시도" 기본값이 적용되지 않습니다. 재시도 정책은 직접 지정하셔야 합니다.

Endpoints

엔드포인트 — 6 스코프 · 8 엔드포인트

모든 엔드포인트는 POST, application/json 이며 필드는 camelCase, enum 값은 소문자입니다. 모든 응답은 non-null 정직성 라벨(label)과 선택적 notice 를 담습니다. 토큰 비용(cost)과 authority 는 요금 페이지와 같은 원천에서 파생됩니다 — 이 문서가 따로 적지 않습니다.

오디언스 핏 4분면 요약

#
POST/partner/v1/audience-fit/summary

자사 실측 전환과 시장 상대지수를 교차해 키워드를 4분면으로 분류한 요약을 냅니다.

scopeaudience_fitauthorityPARTNER_SCOPE_AUDIENCE_FITcost5 토큰

그레인self-scoped — 권위 site 는 grant 로 확정된 값이며, 본문 siteId 로 같은 테넌트의 다른 사이트를 볼 수 없습니다.

기간 제약 — to ≥ from · 최대 180일

요청 필드

PublicAudienceFitRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
fromdate필수—시작일(포함). YYYY-MM-DD.
todate필수—종료일(포함). YYYY-MM-DD.
limitint선택1 이상(서버가 최대 10 으로 재-clamp)topGolden / topTrap 각각의 최대 개수. ★상한 10 은 봉인 상한(SEAL_TOP_N)이라 더 큰 값을 보내도 10 을 넘지 않습니다.

응답 필드

PublicAudienceFitSummaryResponse
필드타입null 조건설명
fromdate산출 구간 미확정집계 시작일.
todate산출 구간 미확정집계 종료일.
baselineCvrdouble자사 baseline CVR 산출 불가(표본 부족) — 이때 caveats 에 사유가 실립니다자사 기준 전환율. 4분면 판정의 기준선입니다.
quadrantCountsobjectnull 아님분면별 카운트. non-null.
totalTrapWastedecimalkeyword-grain 광고비 미연결·미매칭이면 null(0 이 아닙니다)TRAP 분면 추정 낭비액.
hasAdSpendbooleannull 아님광고비가 keyword 그레인으로 연결되었는지. ★false 면 totalTrapWaste 가 null 인 이유입니다.
topGoldenobject[]null 아님GOLDEN 상위 항목(봉인 요약).
topTrapobject[]null 아님TRAP 상위 항목(봉인 요약).
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicQuadrantCounts4분면 카운트

필드타입null 조건설명
goldenintnull 아님GOLDEN 분면 키워드 수.
trapintnull 아님TRAP 분면 키워드 수.
preemptintnull 아님PREEMPT 분면 키워드 수.
ignoreintnull 아님IGNORE 분면 키워드 수.
unevaluatedintnull 아님평가 불가 키워드 수. ★0 으로 뭉개지 않고 별도 카운트로 냅니다 — "측정 못 함"과 "0 건"은 다릅니다.

PublicFitItem상위 핏 항목(봉인 요약 — 상위 N 만)

필드타입null 조건설명
keywordstring소스 값 부재키워드.
quadrantstring평가 불가(정직 — 임의 분면으로 채우지 않습니다)4분면. enum 의 lowercase 문자열('golden' 등).
fitBasisstring판정 근거 부재핏 판정 근거. lowercase 문자열.
ownCvrdouble자사 표본 부족자사 전환율.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-07-01",
  "to": "2026-07-31",
  "limit": 5
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "baselineCvr": 0.0231,
    "quadrantCounts": { "golden": 14, "trap": 9, "preempt": 22, "ignore": 51, "unevaluated": 7 },
    "totalTrapWaste": 1840000,
    "hasAdSpend": true,
    "topGolden": [
      {
        "keyword": "강남 필라테스",
        "quadrant": "golden",
        "fitBasis": "own_cvr",
        "ownCvr": 0.0412,
        "label": {
          "estimated": false,
          "observational": true,
          "isRelativeIndex": true,
          "methodVersion": "audience_fit.summary.v1",
          "dataBasis": "자사 1st-party 행동 실측 × 공개 시장 상대지수 교차",
          "caveats": []
        }
      }
    ],
    "topTrap": [],
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": true,
      "methodVersion": "audience_fit.summary.v1",
      "dataBasis": "자사 1st-party 행동 실측 × 공개 시장 상대지수 교차",
      "caveats": [
        "자사 태깅/표본 부족으로 평가 불가한 시장 키워드 7건(quadrant=null)"
      ]
    }
  }
}

정직성 비고

  • ★민감 처방은 봉인됩니다 — 4분면 카운트 · 집계 · 상위 소수(최대 10)만 나가고 raw 처방 전량은 나가지 않습니다.
  • baselineCvr / totalTrapWaste 는 평가 불가일 때 null 입니다. 0 으로 치환해 렌더하면 "낭비 0원"이라는 없는 사실을 만들게 됩니다.
  • label.isRelativeIndex=true — 시장 모멘텀(상대지수)과 교차한 산출이기 때문입니다.
  • caveats 는 상황에 따라 늘어납니다: baseline 산출 불가 / 평가 불가 시장 키워드 N건 / keyword-grain 광고비 미연결.

이 엔드포인트가 낼 수 있는 에러

큐레이티드 이벤트 피드(keyset 페이지네이션)

#
POST/partner/v1/behavior/events

1st-party 행동 이벤트를 마스킹·가명화한 상태로 시간순 피드로 내려받습니다.

scopebehaviorauthorityPARTNER_SCOPE_BEHAVIORcost5 토큰

그레인self-scoped — 권위 site 는 grant 로 확정됩니다.

기간 제약 — to ≥ from · 최대 31일 (이벤트 그레인이라 다른 슬라이스보다 타이트합니다)

요청 필드

PublicBehaviorEventsRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
frominstant필수—시작 시각. ★Instant 입니다(예: 2026-08-01T00:00:00Z) — 8개 엔드포인트 중 이 엔드포인트만 날짜가 아니라 시각 정밀도입니다. 날짜만 보내면 400 입니다.
toinstant필수—종료 시각(Instant).
eventNamesstring[]선택—이벤트명 필터. 생략 시 전체.
pathstring선택—경로 필터.
subjectstring선택—자기 소유 raw user id 정확매칭 필터. ★서버측 WHERE 바인드로만 쓰이고 응답에 되돌려 실리지 않습니다(에코 없음).
cursorstring선택—직전 응답의 nextCursor(불투명 base64). 첫 페이지에서는 생략합니다.
limitint선택1–200페이지 크기.

응답 필드

PublicBehaviorEventsResponse
필드타입null 조건설명
eventsobject[]null 아님이벤트 배열.
nextCursorstring마지막 페이지면 null다음 페이지 커서.
hasMorebooleannull 아님다음 페이지 존재 여부.
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicBehaviorEvent큐레이티드 이벤트(마스킹 · 가명화 완료)

raw user_id / device_id / server_session_id 가 없습니다. full URL · referrer 원문 · properties blob · IP · user-agent · event_id · tenant_id 도 스키마 레벨에서 부재합니다 — 필터링이 아니라 애초에 조립되지 않습니다.

필드타입null 조건설명
occurredAtinstant소스 값 부재이벤트 발생 시각(Instant).
eventNamestring소스 값 부재이벤트명.
interactionstring소스 값 부재상호작용 종류. lowercase 문자열(예: 'click').
pathstring소스 값 부재경로. ★full URL 이 아니라 경로만입니다.
elementTagstring요소 정보 부재요소 태그.
elementTextstring요소 텍스트 부재요소 텍스트 — 서버강제 PII 마스킹 후 값(카드·주민·이메일 치환, 64자 컷).
platformstring소스 값 부재수집 라이브러리 기준 정규화 플랫폼.
deviceTypestring소스 값 부재기기 종류.
browserstring소스 값 부재브라우저.
osstring소스 값 부재OS.
utmSourcestringutm 미태깅 유입utm_source.
utmMediumstringutm 미태깅 유입utm_medium.
utmCampaignstringutm 미태깅 유입utm_campaign.
referrerHoststring레퍼러 부재 · strict-origin 등으로 미전달레퍼러 호스트만. 전체 URL 은 제공하지 않습니다.
visitorRefstring소스 값 부재방문자 HMAC 가명 — 원본 식별자 복원 불가.
isIdentifiedbooleannull 아님식별 로그인 이용자 여부.
sessionRefstring소스 값 부재세션 HMAC 가명.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

keyset 페이지네이션

★이 엔드포인트만 페이지네이션이 있습니다. 한 번의 호출은 최대 limit(≤200)건만 돌려줍니다 — hasMore 가 true 인데 멈추면 에러 없이 데이터 일부만 받고 전량을 받았다고 착각하게 됩니다. hasMore 가 false 이거나 nextCursor 가 null 이 될 때까지 nextCursor 를 cursor 에 넣어 반복하십시오. ★반복 호출은 매 호출마다 토큰이 차감됩니다(페이지 수 × cost_units).

  • cursor요청 — 이어받을 지점
  • nextCursor응답 — null 이면 마지막 페이지
  • hasMore응답 — 다음 페이지 유무
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-08-01T00:00:00Z",
  "to": "2026-08-16T00:00:00Z",
  "eventNames": ["purchase_complete"],
  "limit": 200
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "events": [
      {
        "occurredAt": "2026-08-15T04:12:33Z",
        "eventName": "purchase_complete",
        "interaction": "click",
        "path": "/order/complete",
        "elementTag": "button",
        "elementText": "결제하기",
        "platform": "web",
        "deviceType": "mobile",
        "browser": "chrome",
        "os": "android",
        "utmSource": "naver",
        "utmMedium": "cpc",
        "utmCampaign": "aug-brand",
        "referrerHost": "search.naver.com",
        "visitorRef": "v_9c1e7b044d2a9f11",
        "isIdentified": false,
        "sessionRef": "s_0c8e5a6b1d33f2a9",
        "label": {
          "estimated": false,
          "observational": true,
          "isRelativeIndex": false,
          "methodVersion": "behavior.events.v1",
          "dataBasis": "자사 1st-party autocapture 행동 실측(근사 식별자)",
          "caveats": [
            "autocapture 근사 — 안정 셀렉터/좌표 부재로 el_tag|el_text 로 요소 식별(위치 무관 합산)",
            "전수 아님 — SPA 라우팅/서버 리다이렉트 폼은 미발화 가능",
            "element_text 는 서버강제 PII 마스킹 후 값(카드/주민/이메일 치환·64자 컷)",
            "표본 부족 행(uniq<5)은 재식별 방지로 봉인됨"
          ]
        }
      }
    ],
    "nextCursor": "eyJvIjoiMjAyNi0wOC0xNVQwNDoxMjozM1oifQ==",
    "hasMore": true,
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "behavior.events.v1",
      "dataBasis": "자사 1st-party autocapture 행동 실측(근사 식별자)",
      "caveats": [
        "autocapture 근사 — 안정 셀렉터/좌표 부재로 el_tag|el_text 로 요소 식별(위치 무관 합산)",
        "전수 아님 — SPA 라우팅/서버 리다이렉트 폼은 미발화 가능",
        "element_text 는 서버강제 PII 마스킹 후 값(카드/주민/이메일 치환·64자 컷)",
        "표본 부족 행(uniq<5)은 재식별 방지로 봉인됨"
      ]
    }
  }
}

정직성 비고

  • ★raw 식별자가 없습니다 — user_id / device_id / server_session_id 대신 HMAC 가명(visitorRef · sessionRef)만 나갑니다.
  • full URL · referrer 원문 · properties blob · IP · user-agent · event_id · tenant_id 는 스키마 레벨에서 부재합니다(응답에서 지우는 것이 아니라 조립되지 않습니다).
  • elementText 는 서버강제 PII 마스킹 후 값입니다(카드·주민·이메일 치환, 64자 컷).
  • autocapture 근사라 전수가 아닙니다 — SPA 라우팅·서버 리다이렉트 폼은 미발화할 수 있습니다.
  • 무효한 cursor 는 도메인 실패로 떨어지며, 그 호출은 과금되지 않습니다(도메인 성공 플래그 미설정 → 예약 해제).

이 엔드포인트가 낼 수 있는 에러

활동 요약 + top 클릭 요소

#
POST/partner/v1/behavior/summary

기간 활동 집계와 가장 많이 눌린 요소를 냅니다.

scopebehaviorauthorityPARTNER_SCOPE_BEHAVIORcost5 토큰

그레인self-scoped — 권위 site 는 grant 로 확정됩니다.

기간 제약 — to ≥ from · 최대 31일

요청 필드

PublicBehaviorSummaryRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
fromdate필수—시작일(포함). ★이쪽은 날짜 그레인입니다 — 같은 behavior 스코프라도 events 와 타입이 다릅니다.
todate필수—종료일(포함).
limitint선택1–10top 요소 개수. ★events 의 1–200 과 다른 상한입니다.

응답 필드

PublicBehaviorSummaryResponse
필드타입null 조건설명
fromdate산출 구간 미확정집계 시작일.
todate산출 구간 미확정집계 종료일.
activityobjectnull 아님활동 집계. 객체 자체는 non-null 이지만 내부 전 필드가 nullable 입니다.
topElementsobject[]null 아님top 클릭 요소. k-익명 미달 시 빈 배열 + notice.
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicBehaviorActivity활동 집계

★전 필드 nullable 입니다. 미수집·수집 초기를 0 으로 위장하지 않기 위한 의도된 셰이프입니다 — null 을 0 으로 치환해 렌더하지 마십시오.

필드타입null 조건설명
visitorslong미수집 · 수집 초기(0 과 다릅니다)순 방문자.
sessionslong미수집 · 수집 초기세션 수.
pageviewslong미수집 · 수집 초기페이지뷰.
engagedSecondslong미수집 · 수집 초기체류(참여) 초 합.
avgDwellSecondsdouble미수집 · 수집 초기평균 체류 초.

PublicClickedElementtop 클릭 요소(마스킹 · uniq 카운트)

필드타입null 조건설명
pathstring소스 값 부재경로.
elementTagstring요소 정보 부재요소 태그.
elementTextstring요소 텍스트 부재요소 텍스트(PII 마스킹 후).
interactionstring소스 값 부재상호작용 종류. lowercase 문자열.
clickslong집계 불가클릭 수.
sessionslong집계 불가해당 요소를 만진 세션 수.
visitorslong집계 불가해당 요소를 만진 순 방문자 수.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-08-01",
  "to": "2026-08-16",
  "limit": 10
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-08-01",
    "to": "2026-08-16",
    "activity": {
      "visitors": 4820,
      "sessions": 6104,
      "pageviews": 19338,
      "engagedSeconds": 412900,
      "avgDwellSeconds": 67.6
    },
    "topElements": [
      {
        "path": "/product/1024",
        "elementTag": "button",
        "elementText": "장바구니 담기",
        "interaction": "click",
        "clicks": 1204,
        "sessions": 902,
        "visitors": 811,
        "label": {
          "estimated": false,
          "observational": true,
          "isRelativeIndex": false,
          "methodVersion": "behavior.summary.v1",
          "dataBasis": "자사 1st-party autocapture 행동 실측(근사 식별자)",
          "caveats": [
            "autocapture 근사 — 안정 셀렉터/좌표 부재로 el_tag|el_text 로 요소 식별(위치 무관 합산)",
            "전수 아님 — SPA 라우팅/서버 리다이렉트 폼은 미발화 가능",
            "element_text 는 서버강제 PII 마스킹 후 값(카드/주민/이메일 치환·64자 컷)",
            "표본 부족 행(uniq<5)은 재식별 방지로 봉인됨"
          ]
        }
      }
    ],
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "behavior.summary.v1",
      "dataBasis": "자사 1st-party autocapture 행동 실측(근사 식별자)",
      "caveats": [
        "autocapture 근사 — 안정 셀렉터/좌표 부재로 el_tag|el_text 로 요소 식별(위치 무관 합산)",
        "전수 아님 — SPA 라우팅/서버 리다이렉트 폼은 미발화 가능",
        "element_text 는 서버강제 PII 마스킹 후 값(카드/주민/이메일 치환·64자 컷)",
        "표본 부족 행(uniq<5)은 재식별 방지로 봉인됨"
      ]
    }
  }
}
응답 — k-익명 봉인 (장애가 아닙니다)json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-08-01",
    "to": "2026-08-16",
    "activity": {
      "visitors": null,
      "sessions": null,
      "pageviews": null,
      "engagedSeconds": null,
      "avgDwellSeconds": null
    },
    "topElements": [],
    "notice": "표본이 부족해 표시할 top 요소가 없습니다.",
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "behavior.summary.v1",
      "dataBasis": "자사 1st-party autocapture 행동 실측(근사 식별자)",
      "caveats": [
        "autocapture 근사 — 안정 셀렉터/좌표 부재로 el_tag|el_text 로 요소 식별(위치 무관 합산)",
        "전수 아님 — SPA 라우팅/서버 리다이렉트 폼은 미발화 가능",
        "element_text 는 서버강제 PII 마스킹 후 값(카드/주민/이메일 치환·64자 컷)",
        "표본 부족 행(uniq<5)은 재식별 방지로 봉인됨"
      ]
    }
  }
}

정직성 비고

  • ★activity 의 전 필드가 nullable 입니다 — 미수집·수집 초기를 0 으로 위장하지 않습니다. null 을 0 으로 치환해 렌더하면 "방문자 0명"이라는 없는 사실이 만들어집니다.
  • top 요소는 k-익명(HAVING)을 통과한 행만 나옵니다. 통과 행이 없으면 빈 배열 + notice 로 그 사실을 밝힙니다(장애가 아닙니다).
  • autocapture 근사라 전수가 아닙니다(caveats 동일 4종).

이 엔드포인트가 낼 수 있는 에러

집계 전환 · 매출 지표

#
POST/partner/v1/conversions/summary

기간 전환·매출을 집계값으로 냅니다(개별 주문 행 없음).

scopeconversionsauthorityPARTNER_SCOPE_CONVERSIONScost8 토큰

그레인self-scoped — order_id 멱등 매출 집계. 집계만 나가며 개별 주문·유저 행은 없습니다.

기간 제약 — to ≥ from · 최대 180일

요청 필드

PublicConversionSummaryRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
fromdate필수—시작일(포함).
todate필수—종료일(포함).

응답 필드

PublicConversionSummaryResponse
필드타입null 조건설명
fromdate산출 구간 미확정집계 시작일.
todate산출 구간 미확정집계 종료일.
visitorslong집계 불가총 유입 방문자. ★민감 구간이 아니라 봉인 대상이 아닙니다 — 봉인 응답에서도 이 값만 남습니다.
conversionslongk-익명 봉인(순 구매자 uniq < 5) 시 null + notice구매 전환 순 방문자.
conversionRatedoublek-익명 봉인 시 · 또는 visitors 가 0conversions ÷ visitors(소수 4자리 반올림).
orderslongk-익명 봉인 시 null주문 수(order_id 멱등).
revenueTotaldecimalk-익명 봉인 시 null총매출(gross).
aovdecimalk-익명 봉인 시 · 또는 orders 가 0객단가 = revenueTotal ÷ orders(원 단위 반올림).
currencystringk-익명 봉인 시 null통화 코드('KRW').
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-07-01",
  "to": "2026-07-31"
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "visitors": 4820,
    "conversions": 214,
    "conversionRate": 0.0444,
    "orders": 231,
    "revenueTotal": 18734000,
    "aov": 81099,
    "currency": "KRW",
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "conversions.summary.v1",
      "dataBasis": "자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)",
      "caveats": [
        "구매완료는 주문완료 페이지 서버렌더 실측값 — order_id 멱등(재발행·재시도·크로스세션 중복은 주문당 1회 집계)",
        "utm 미태깅 유입은 채널 오귀속 가능 — 본 집계는 채널 무관 전 사이트 전환 총량",
        "환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)",
        "순 구매자(uniq<5) 구간은 재식별 방지로 전환·매출 지표가 봉인됨"
      ]
    }
  }
}
응답 — k-익명 봉인 (장애가 아닙니다)json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "visitors": 312,
    "conversions": null,
    "conversionRate": null,
    "orders": null,
    "revenueTotal": null,
    "aov": null,
    "currency": null,
    "notice": "순 구매자(uniq)가 5명 미만이라 재식별 방지를 위해 전환·매출 지표를 봉인했습니다. 방문자 총량만 표시됩니다.",
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "conversions.summary.v1",
      "dataBasis": "자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)",
      "caveats": [
        "구매완료는 주문완료 페이지 서버렌더 실측값 — order_id 멱등(재발행·재시도·크로스세션 중복은 주문당 1회 집계)",
        "utm 미태깅 유입은 채널 오귀속 가능 — 본 집계는 채널 무관 전 사이트 전환 총량",
        "환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)",
        "순 구매자(uniq<5) 구간은 재식별 방지로 전환·매출 지표가 봉인됨"
      ]
    }
  }
}

정직성 비고

  • ★k-익명 봉인: 순 구매자(uniq device)가 5명 미만이면 전환·매출 블록 전체가 null 이 되고 notice 로 사유를 밝힙니다. visitors 만 남습니다. 이건 장애가 아니라 정상 응답입니다.
  • order_id 멱등 집계 — 재발행·재시도·크로스세션 중복은 주문당 1회만 셉니다.
  • ★환불·취소 보정이 반영되지 않은 gross 기준입니다.
  • utm 미태깅 유입은 채널 오귀속이 가능합니다 — 이 집계는 채널 무관 전 사이트 전환 총량입니다.
  • order_id · 결제수단 · 구매자 정보는 어디에도 없습니다(스키마 레벨 부재).

이 엔드포인트가 낼 수 있는 에러

파트너 정의 퍼널

#
POST/partner/v1/conversions/funnel

보내주신 이벤트명 순서대로 단계별 도달·이탈·전환율을 냅니다.

scopeconversionsauthorityPARTNER_SCOPE_CONVERSIONScost8 토큰

그레인self-scoped — 세션 그레인 집계만. 개별 세션·유저 행은 없습니다.

기간 제약 — to ≥ from · 최대 92일 (세션 그레인 스캔이라 summary 의 180일보다 타이트합니다)

요청 필드

PublicConversionFunnelRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
fromdate필수—시작일(포함).
todate필수—종료일(포함).
stepsstring[]필수2~8개 · 각 비공백 · 각 ≤120자퍼널 단계 이벤트명(보낸 순서대로 판정). 자유 입력이지만 서버측 파라미터 바인딩이라 쿼리에 문자열로 끼워 넣지 않습니다.

응답 필드

PublicConversionFunnelResponse
필드타입null 조건설명
fromdate산출 구간 미확정집계 시작일.
todate산출 구간 미확정집계 종료일.
stepsobject[]null 아님단계별 결과. ★퍼널 진입 세션 < 5 이면 빈 배열로 전량 봉인됩니다.
overallConversionRatedouble마지막 단계 도달 세션 < 5 (비율로 소표본 카운트를 역산하는 것을 막습니다) · 또는 진입 0마지막 단계 ÷ 첫 단계.
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicFunnelStep퍼널 단계(집계)

단계별 k-익명: 도달 세션이 5 미만인 단계는 reached / dropOff / stepRate 가 null 로 봉인되고 step 이름만 남습니다. 퍼널 reached 는 단조감소라 한 단계가 소표본이면 이후 단계도 소표본입니다.

필드타입null 조건설명
stepstring소스 값 부재요청에 넣은 이벤트명 그대로.
reachedlong단계 도달 세션 < 5 (k-익명 봉인)이 단계에 도달한 세션 수.
dropOfflong첫 단계 · 또는 단계 봉인직전 단계 대비 이탈 세션 수.
stepRatedouble첫 단계(직전이 없음) · 또는 단계 봉인직전 단계 대비 전환율. ★첫 단계는 원리적으로 null 입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-07-01",
  "to": "2026-07-31",
  "steps": ["view_product", "add_to_cart", "begin_checkout", "purchase_complete"]
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "steps": [
      { "step": "view_product",     "reached": 6104, "dropOff": null, "stepRate": null,   "label": { "estimated": false, "observational": true, "isRelativeIndex": false, "methodVersion": "conversions.funnel.v1", "dataBasis": "자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)", "caveats": ["구매완료는 주문완료 페이지 서버렌더 실측값 — order_id 멱등(재발행·재시도·크로스세션 중복은 주문당 1회 집계)", "utm 미태깅 유입은 채널 오귀속 가능 — 본 집계는 채널 무관 전 사이트 전환 총량", "환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)", "순 구매자(uniq<5) 구간은 재식별 방지로 전환·매출 지표가 봉인됨"] } },
      { "step": "add_to_cart",      "reached": 1902, "dropOff": 4202, "stepRate": 0.3116, "label": { "estimated": false, "observational": true, "isRelativeIndex": false, "methodVersion": "conversions.funnel.v1", "dataBasis": "자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)", "caveats": ["구매완료는 주문완료 페이지 서버렌더 실측값 — order_id 멱등(재발행·재시도·크로스세션 중복은 주문당 1회 집계)", "utm 미태깅 유입은 채널 오귀속 가능 — 본 집계는 채널 무관 전 사이트 전환 총량", "환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)", "순 구매자(uniq<5) 구간은 재식별 방지로 전환·매출 지표가 봉인됨"] } }
    ],
    "overallConversionRate": 0.0389,
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "conversions.funnel.v1",
      "dataBasis": "자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)",
      "caveats": [
        "구매완료는 주문완료 페이지 서버렌더 실측값 — order_id 멱등(재발행·재시도·크로스세션 중복은 주문당 1회 집계)",
        "utm 미태깅 유입은 채널 오귀속 가능 — 본 집계는 채널 무관 전 사이트 전환 총량",
        "환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)",
        "순 구매자(uniq<5) 구간은 재식별 방지로 전환·매출 지표가 봉인됨"
      ]
    }
  }
}
응답 — k-익명 봉인 (장애가 아닙니다)json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "steps": [],
    "overallConversionRate": null,
    "notice": "퍼널 진입 세션이 5개 미만이라 재식별 방지를 위해 단계 지표를 봉인했습니다.",
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "conversions.funnel.v1",
      "dataBasis": "자사 1st-party 서버렌더 구매완료 실측(order_id 멱등 집계)",
      "caveats": [
        "구매완료는 주문완료 페이지 서버렌더 실측값 — order_id 멱등(재발행·재시도·크로스세션 중복은 주문당 1회 집계)",
        "utm 미태깅 유입은 채널 오귀속 가능 — 본 집계는 채널 무관 전 사이트 전환 총량",
        "환불/취소 보정 미반영(gross) — 총매출 기준(음수 보정은 후속)",
        "순 구매자(uniq<5) 구간은 재식별 방지로 전환·매출 지표가 봉인됨"
      ]
    }
  }
}

정직성 비고

  • ★k-익명 2단: 퍼널 진입 세션이 5 미만이면 steps 가 빈 배열 + notice(전량 봉인). 진입은 넘겼어도 개별 단계 도달이 5 미만이면 그 단계의 reached / dropOff / stepRate 만 null 로 봉인됩니다.
  • overallConversionRate 도 마지막 단계가 소표본이면 null 입니다 — 비율만 남기면 소표본 카운트를 역산할 수 있기 때문입니다.
  • stepRate 는 첫 단계에서 원리적으로 null 입니다(직전 단계가 없습니다).
  • 세션 단위 카운트만 제공합니다 — 개별 세션·유저 행은 없습니다.

이 엔드포인트가 낼 수 있는 에러

채널 롤업 광고 지표

#
POST/partner/v1/ads/summary

광고 계정의 채널 단위 노출·클릭·비용·전환 롤업을 냅니다.

scopeadsauthorityPARTNER_SCOPE_ADScost3 토큰

그레인★account-level(테넌트 광고계정 전체 합산 — site 귀속 안 함). 8개 중 이 엔드포인트만 self-scoped 가 아닙니다. siteId 는 grant 핀 키일 뿐이며 집계를 그 사이트로 좁히지 않습니다 — "이 사이트의 광고비"로 읽으면 틀립니다.

기간 제약 — to ≥ from · 최대 180일

요청 필드

PublicAdsSummaryRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
fromdate필수—시작일(포함).
todate필수—종료일(포함).

응답 필드

PublicAdsSummaryResponse
필드타입null 조건설명
fromdate산출 구간 미확정집계 시작일.
todate산출 구간 미확정집계 종료일.
channelsobject[]null 아님채널 롤업. 무집행이거나 전량 봉인이면 빈 배열 + notice.
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicAdsChannelRollup채널 롤업 한 행(coarse)

개별 keyword / entity_id / siteId / tenant_key 가 없습니다. 지표는 버킷 반올림된 값이라 원값이 아닙니다.

필드타입null 조건설명
channelstring채널 키 부재 행은 애초에 제외됩니다채널. enum 의 lowercase 문자열.
campaignNamestring★v1 에서는 항상 null — 채널 그레인만 제공합니다(캠페인·키워드 그레인 미노출)캠페인명. 필드는 있으나 v1 은 채널 그레인이라 값이 채워지지 않습니다.
impressionslong집계 불가노출. 100 단위 반올림.
clickslong집계 불가클릭. 10 단위 반올림.
costdecimal집계 불가광고비(KRW). 1,000원 단위 반올림.
conversionslong집계 불가전환. 5 단위 반올림.
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-07-01",
  "to": "2026-07-31"
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "channels": [
      {
        "channel": "naver",
        "campaignName": null,
        "impressions": 412300,
        "clicks": 8940,
        "cost": 6841000,
        "conversions": 215
      },
      {
        "channel": "google",
        "campaignName": null,
        "impressions": 118600,
        "clicks": 2310,
        "cost": 2094000,
        "conversions": 60
      }
    ],
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "ads.summary.v1",
      "dataBasis": "자사 광고계정 커넥터 수집 실측(unified_metrics 채널 롤업, account-level)",
      "caveats": [
        "account-level aggregate; not per-site; figures rounded/k-anonymized",
        "채널(+캠페인) 롤업만 제공 — 개별 keyword/entity 행은 미노출(coarse grain)",
        "광고계정 전체 합산이라 site 별 귀속은 하지 않음(site_id 는 grant 키일 뿐 집계 스코프 아님)",
        "impressions/clicks/conversions 는 버킷 반올림, cost 는 KRW 반올림 — 소버킷 채널은 재식별 방지로 봉인"
      ]
    }
  }
}
응답 — k-익명 봉인 (장애가 아닙니다)json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-07-01",
    "to": "2026-07-31",
    "channels": [],
    "notice": "해당 기간 집계 가능한 광고 채널이 없습니다(무집행 또는 지표가 반올림 하한 미만이라 재식별 방지로 제외).",
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "ads.summary.v1",
      "dataBasis": "자사 광고계정 커넥터 수집 실측(unified_metrics 채널 롤업, account-level)",
      "caveats": [
        "account-level aggregate; not per-site; figures rounded/k-anonymized",
        "채널(+캠페인) 롤업만 제공 — 개별 keyword/entity 행은 미노출(coarse grain)",
        "광고계정 전체 합산이라 site 별 귀속은 하지 않음(site_id 는 grant 키일 뿐 집계 스코프 아님)",
        "impressions/clicks/conversions 는 버킷 반올림, cost 는 KRW 반올림 — 소버킷 채널은 재식별 방지로 봉인"
      ]
    }
  }
}

정직성 비고

  • ★account-level 집계입니다 — 광고계정 전체 합산이며 site 별 귀속을 하지 않습니다. 사이트별 광고비로 렌더하면 없는 사실을 만드는 것입니다.
  • ★지표는 버킷 반올림된 값입니다: 노출 100 · 클릭 10 · 전환 5 · 광고비 1,000원 단위. 원값이 아니므로 정산·청구 근거로 쓸 수 없습니다.
  • 반올림 후 전 지표가 0 이 되는 소액 채널은 재식별 방지로 행 자체가 제외됩니다 — 채널이 사라진 것이 아니라 봉인된 것입니다.
  • campaignName 은 v1 에서 항상 null 입니다(채널 그레인만 제공). 개별 keyword / entity 행은 없습니다.

이 엔드포인트가 낼 수 있는 에러

키워드 예산 최적화 요약

#
POST/partner/v1/keyword-budget/summary

자사 검색량·경쟁도 데이터와 테넌트의 실집행 CPL/CPA 를 교차해 절감·증액·미확보 후보를 냅니다.

scopekeyword_budgetauthorityPARTNER_SCOPE_KEYWORD_BUDGETcost13 토큰

그레인self-scoped. ★재사용 서비스가 grant 로 확정된 테넌트의 구독 티어가 PREMIUM 인지를 추가로 요구합니다 — 미달이면 데이터가 나오지 않고, 도메인 성공 플래그가 서지 않으므로 과금도 되지 않습니다.

기간 제약 — to ≥ from · 최대 180일

요청 필드

PublicKeywordBudgetRequest
필드타입필수제약설명
siteIduuid필수—grant 로 검증되는 사이트 UUID. 우리 analytics_sites 에서 소유 테넌트를 해소해 grant 를 찾습니다 — 미존재·비활성·미부여는 전부 동일한 403 입니다(사이트 존재 여부가 오라클이 되면 안 되기 때문).
fromdate필수—시작일(포함).
todate필수—종료일(포함).

응답 필드

PublicKeywordBudgetResponse
필드타입null 조건설명
fromdate산출 구간 미확정집계 시작일.
todate산출 구간 미확정집계 종료일.
countsobjectnull 아님갈래별 건수 집계. non-null.
topSavingsobject[]null 아님절감 후보 상위(최대 20).
topScaleUpobject[]null 아님증액 후보 상위(최대 20).
topUntappedobject[]null 아님미확보 키워드 상위(최대 20).
noticestring고지할 사유가 없으면 null(정상)k-익명 봉인·데이터 부족 등으로 값이 비었을 때 그 사유. null 이면 고지 사항 없음입니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicKeywordBudgetCounts갈래별 건수 + 집행 키워드 수

필드타입null 조건설명
savingsintnull 아님절감 후보 건수.
scaleUpintnull 아님증액 후보 건수.
untappedintnull 아님미확보(검색량 보유 · 미집행) 건수.
executedintnull 아님해당 기간 실집행 키워드 수. ★0 이면 절감·증액 처방이 원리적으로 산출되지 않습니다(caveats 에 그 사실이 추가됩니다).

PublicKeywordBudgetItem처방 후보(봉인 요약 — 상위 N 만)

★효율 rate(cpl / cpa / cvr) · 검색량 · 경쟁도 · 사유만 있습니다. 광고비 총액 · 매출 · 전환 raw 카운트는 매퍼가 의도적으로 드롭합니다 — 요청한다고 나오는 값이 아닙니다.

필드타입null 조건설명
keywordstring소스 값 부재키워드.
monthlySearchTotallong검색량 데이터 부재월 검색량. ★자사 수집 추정치입니다(실측인 cpl/cpa/cvr 과 근거가 다릅니다).
competitionstring경쟁도 산출 불가경쟁도. lowercase 문자열.
cpldecimal실집행 표본 부족실집행 기반 CPL(실측).
cpadecimal실집행 표본 부족실집행 기반 CPA(실측).
cvrdecimal실집행 표본 부족실집행 기반 전환율(실측).
reasonstring사유 부재처방 사유. lowercase 문자열. ★'고/저' 판정은 자사 분포 중앙값 기준 self-relative 이며 절대 임계가 아닙니다.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).

PublicUntappedKeywordItem미확보 키워드(검색량 보유 · 미집행)

★근거는 검색량뿐입니다. 실집행 신호가 없어 cpl/cpa/cvr 필드 자체가 없습니다 — 이건 예측이 아니라 "아직 집행하지 않았다"는 관측입니다.

필드타입null 조건설명
keywordstring소스 값 부재키워드.
monthlySearchTotallong검색량 데이터 부재월 검색량(자사 수집 추정치).
competitionstring경쟁도 산출 불가경쟁도. lowercase 문자열.
labelobjectnull 아님정직성 라벨. non-null 관통 — 소스 라벨이 비면 매퍼가 조립 단계에서 명시 실패합니다(라벨 없는 응답이 새지 않습니다).
요청json
{
  "siteId": "<SITE_ID>",
  "from": "2026-08-01",
  "to": "2026-08-16"
}
응답json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-08-01",
    "to": "2026-08-16",
    "counts": { "savings": 12, "scaleUp": 7, "untapped": 23, "executed": 318 },
    "topSavings": [
      {
        "keyword": "필라테스 가격",
        "monthlySearchTotal": 18300,
        "competition": "high",
        "cpl": 12400,
        "cpa": 68200,
        "cvr": 0.0182,
        "reason": "cpa_above_median",
        "label": {
          "estimated": false,
          "observational": true,
          "isRelativeIndex": false,
          "methodVersion": "keyword_budget.summary.v1",
          "dataBasis": "자사 키워드 검색량 데이터 × 테넌트 실집행 CPL/CPA 교차",
          "caveats": [
            "효율 rate(cpl/cpa/cvr)·검색량·경쟁도만 제공 — 개별 광고비 총액/매출/전환 raw 카운트는 미노출(집계·봉인)",
            "검색량은 자사 수집 추정치(월 검색량), 실집행 지표(cpl/cpa/cvr)는 실측 — 근거가 다름",
            "'고/저' 판정은 자사 분포 중앙값 기준 self-relative(절대 임계 아님)",
            "미확보(untapped)는 검색량만 근거 — 해당 키워드의 실집행/전환 실적은 없음(예측 아님, 미집행 관측)",
            "top-N 봉인 요약만 제공 — 전체 처방 목록은 미노출"
          ]
        }
      }
    ],
    "topScaleUp": [],
    "topUntapped": [
      {
        "keyword": "강남 필라테스 소도구",
        "monthlySearchTotal": 2400,
        "competition": "low",
        "label": {
          "estimated": false,
          "observational": true,
          "isRelativeIndex": false,
          "methodVersion": "keyword_budget.summary.v1",
          "dataBasis": "자사 키워드 검색량 데이터 × 테넌트 실집행 CPL/CPA 교차",
          "caveats": [
            "효율 rate(cpl/cpa/cvr)·검색량·경쟁도만 제공 — 개별 광고비 총액/매출/전환 raw 카운트는 미노출(집계·봉인)",
            "검색량은 자사 수집 추정치(월 검색량), 실집행 지표(cpl/cpa/cvr)는 실측 — 근거가 다름",
            "'고/저' 판정은 자사 분포 중앙값 기준 self-relative(절대 임계 아님)",
            "미확보(untapped)는 검색량만 근거 — 해당 키워드의 실집행/전환 실적은 없음(예측 아님, 미집행 관측)",
            "top-N 봉인 요약만 제공 — 전체 처방 목록은 미노출"
          ]
        }
      }
    ],
    "notice": null,
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "keyword_budget.summary.v1",
      "dataBasis": "자사 키워드 검색량 데이터 × 테넌트 실집행 CPL/CPA 교차",
      "caveats": [
        "효율 rate(cpl/cpa/cvr)·검색량·경쟁도만 제공 — 개별 광고비 총액/매출/전환 raw 카운트는 미노출(집계·봉인)",
        "검색량은 자사 수집 추정치(월 검색량), 실집행 지표(cpl/cpa/cvr)는 실측 — 근거가 다름",
        "'고/저' 판정은 자사 분포 중앙값 기준 self-relative(절대 임계 아님)",
        "미확보(untapped)는 검색량만 근거 — 해당 키워드의 실집행/전환 실적은 없음(예측 아님, 미집행 관측)",
        "top-N 봉인 요약만 제공 — 전체 처방 목록은 미노출"
      ]
    }
  }
}
응답 — k-익명 봉인 (장애가 아닙니다)json
{
  "code": "200",
  "message": "Success",
  "data": {
    "from": "2026-08-01",
    "to": "2026-08-16",
    "counts": { "savings": 0, "scaleUp": 0, "untapped": 0, "executed": 0 },
    "topSavings": [],
    "topScaleUp": [],
    "topUntapped": [],
    "notice": "해당 기간 처방 가능한 키워드가 없습니다(실집행 또는 검색량 데이터 부족).",
    "label": {
      "estimated": false,
      "observational": true,
      "isRelativeIndex": false,
      "methodVersion": "keyword_budget.summary.v1",
      "dataBasis": "자사 키워드 검색량 데이터 × 테넌트 실집행 CPL/CPA 교차",
      "caveats": [
        "효율 rate(cpl/cpa/cvr)·검색량·경쟁도만 제공 — 개별 광고비 총액/매출/전환 raw 카운트는 미노출(집계·봉인)",
        "검색량은 자사 수집 추정치(월 검색량), 실집행 지표(cpl/cpa/cvr)는 실측 — 근거가 다름",
        "'고/저' 판정은 자사 분포 중앙값 기준 self-relative(절대 임계 아님)",
        "미확보(untapped)는 검색량만 근거 — 해당 키워드의 실집행/전환 실적은 없음(예측 아님, 미집행 관측)",
        "top-N 봉인 요약만 제공 — 전체 처방 목록은 미노출",
        "집계 기간 실집행 키워드가 없어 절감/증액 처방을 산출할 수 없습니다(미확보만 가능)."
      ]
    }
  }
}

정직성 비고

  • ★raw 광고비 총액 · 매출 · 전환 raw 카운트는 스키마 레벨에서 부재합니다. 나오는 것은 효율 rate(cpl/cpa/cvr) · 검색량 · 경쟁도 · 처방 사유뿐입니다.
  • 검색량은 자사 수집 추정치이고 실집행 지표(cpl/cpa/cvr)는 실측입니다 — 한 응답 안에 근거가 다른 두 종류가 섞여 있습니다.
  • '고 / 저' 판정은 자사 분포 중앙값 기준 self-relative 입니다. 업계 절대 임계가 아닙니다.
  • 미확보(untapped)는 검색량만 근거입니다 — 그 키워드의 실집행·전환 실적이 없다는 뜻이며, 성과를 예측한 값이 아닙니다.
  • 각 목록은 상위 20건으로 봉인됩니다(SEAL_TOP_N). 전체 처방 목록은 제공하지 않습니다.
  • ★대상 테넌트가 PREMIUM 이 아니면 데이터가 나오지 않으며 그 호출은 과금되지 않습니다.
  • 실집행 키워드가 0 이면 절감·증액 처방이 원리적으로 산출되지 않고(미확보만 가능), 그 사실이 caveats 에 한 줄 추가됩니다.

이 엔드포인트가 낼 수 있는 에러

Errors

에러 카탈로그

위 표에서 401 · 403 · 402 · 503 · 400(PT005) · 429 는 인증 필터가 직접 내보내는 응답이라 실제 HTTP 상태코드가 그대로 나갑니다(필터는 전역 예외 핸들러 밖에서 실행됩니다). 반면 본문 검증 실패와 컨트롤러 도메인 실패는 전역 규약을 타서 HTTP 200 으로 나가고, 실패라는 사실은 envelope 의 code 필드에만 있습니다. 성공 판정을 HTTP 상태로 하지 마시고 code 값이 "200" 인지로 하십시오.

코드HTTP의미복구 행동재시도순 과금동반 헤더
PT001
PARTNER_UNAUTHENTICATED
401인증 실패. 알 수 없거나 정지·만료된 클라이언트, 시크릿 불일치, 그리고 모든 서명 경로 실패(서명 누락·위조, 시각 ±300초 이탈, nonce 형식 오류, nonce 재사용, 서명키 미발급, legacy sunset 경과, 본문 256KB 초과)가 전부 이 하나로 떨어집니다. ★어느 검사에서 걸렸는지는 응답에 실리지 않습니다 — 서명 스킴에 오라클을 주지 않기 위한 의도된 설계입니다.「401 자가 진단」 절 순서대로 좁히십시오. 첫 번째로 볼 것은 코드가 아니라 시계입니다(X-Partner-Server-Time 과 내 epoch 차이). 그래도 안 되면 실패한 요청의 정규화 문자열 8줄을 문의에 첨부하십시오(★서명키는 절대 보내지 마십시오).불가 — 원인을 고치기 전에는 같은 결과0 토큰X-Partner-Server-Time
PT002
PARTNER_GRANT_DENIED
403권한 없음. grant 0건 / 스코프 불일치 / 대상 테넌트 비활성 / siteId 가 우리 기록에 없거나 비활성인 사이트 / 스코프를 해소할 수 없는 경로 / POST 가 아닌 메서드. ★이 사유들은 서로 구분되지 않습니다 — 사이트 존재 여부가 오라클이 되면 안 되기 때문입니다.인증은 통과한 상태입니다. 그 siteId 가 부여받은 테넌트의 사이트가 맞는지, 그 스코프가 grant 에 포함되어 있는지 확인하시고, 맞다면 담당자에게 문의하십시오 — grant 는 우리 쪽에서만 부여할 수 있습니다.불가 — 원인을 고치기 전에는 같은 결과0 토큰X-Partner-Server-Time
PT003
PARTNER_QUOTA_EXCEEDED
402월 토큰 한도 초과. 서빙 전 reserve 단계의 하드컷입니다(used + cost > limit). ★월 한도가 설정되지 않은 크리덴셜은 한도가 0 으로 간주되어 첫 호출부터 이 코드가 납니다(전역 기본 시드가 없습니다 — fail-CLOSED).월 한도 증액은 담당자 문의가 유일한 경로입니다. 이 응답에도 X-Quota-Remaining 과 X-Quota-Reset 이 실리므로 얼마나 남았고 언제 풀리는지는 응답만으로 알 수 있습니다(리셋은 KST 월경계 — 다음 달 1일 0시). 미리 예고를 받으시려면 매 호출의 X-Quota-Remaining 을 임계값과 비교하십시오.불가 — 원인을 고치기 전에는 같은 결과0 토큰X-Partner-Server-Time
PT004
PARTNER_METERING_UNAVAILABLE
503일시 오류. 미터링 저장소 장애, 또는 그 상품의 토큰 단가 행이 없음(단가 미시드).지수 백오프로 재시도하십시오. 특정 엔드포인트에서만 계속 재현되면 단가 미시드일 가능성이 높고, 그건 우리 쪽 설정이므로 문의해 주셔야 풀립니다.가능0 토큰X-Partner-Server-Time
PT005
PARTNER_SITE_REQUIRED
400body 의 siteId 가 없거나 UUID 로 읽히지 않음. ★필드명은 camelCase siteId 입니다(site_id 아님).siteId 를 camelCase UUID 로 넣으십시오. ★이 검사는 크리덴셜 검증보다 앞에서 일어납니다 — 이 코드가 났다고 해서 인증이 통과했다는 뜻은 아닙니다.불가 — 원인을 고치기 전에는 같은 결과0 토큰X-Partner-Server-Time
PT009
PARTNER_RATE_LIMITED
인증 실패 스로틀
429같은 출발지 IP 에서 60초 창 안에 인증 실패가 30건을 넘었습니다(서명 무차별 대입 방어). ★이 게이트는 상시 켜져 있고 모든 크리덴셜 검사보다 앞에 있습니다 — 401 을 반복하다 429 로 바뀌는 것이 이것입니다. 버킷 키가 client_id 가 아니라 IP 인 이유는, client_id 가 공개값이라 남의 client_id 로 난사해 정상 파트너를 묶는 그리핑이 성립하기 때문입니다.Retry-After 헤더만큼 기다리면 카운터가 소멸해 스스로 풀립니다 — 잠금이 아닙니다. 그 사이에 골든 벡터로 서명 구현부터 오프라인 검증하십시오.Retry-After 만큼 대기 후0 토큰Retry-After · X-Partner-Server-Time
PT009
PARTNER_RATE_LIMITED
초당 버스트
429인증 통과 후 reserve 앞에 있는 client_id 축 초당 상한. ★이 게이트는 그 클라이언트에 burst_rps 가 설정된 경우에만 동작합니다 — 미설정이면 게이트를 건너뜁니다. 현재 값이 설정된 클라이언트는 없습니다(ARMED-BUT-UNSET).초당 호출 속도를 낮추고 재시도하십시오. ★이 갈래에는 Retry-After 가 붙지 않습니다 — 429 에 Retry-After 가 없으면 인증 실패 스로틀이 아니라 버스트 쪽입니다(두 갈래를 구분하는 실용적 단서).가능0 토큰X-Partner-Server-Time
VALIDATION
INVALID_INPUT (envelope code "400")
200요청 본문 검증 실패 — 기간 캡 초과(to < from 포함), steps 개수·길이 위반, limit 범위 위반, 타입 불일치, 필수 필드 누락, JSON 파싱 오류. ★★HTTP 상태는 200 입니다. 이 API 는 도메인 결과를 전역 envelope 으로 내보내며, 실패도 200 으로 나갑니다. HTTP 상태만 보는 클라이언트는 이 실패를 성공으로 삼킵니다 — 반드시 envelope 의 code 가 "200" 인지 확인하십시오.message 에 어느 필드가 왜 틀렸는지가 실려 옵니다. 기간 캡·limit 범위는 엔드포인트 레퍼런스의 제약 열을 보십시오.불가 — 원인을 고치기 전에는 같은 결과0 토큰X-Partner-Server-Time

PT006(PARTNER_CLIENT_NOT_FOUND) · PT007(PARTNER_GRANT_NOT_FOUND) · PT008(PARTNER_INVALID_SCOPE)은 존재하지만 운영자 프로비저닝 전용이라 파트너 요청의 응답으로는 나오지 않습니다. 위 표가 /partner/v1 호출에서 실제로 받을 수 있는 전부입니다.

에러 envelope — 필터가 낸 403http
HTTP/1.1 403 Forbidden
Content-Type: application/json
X-Partner-Server-Time: 1755400123

{
  "code": "PT002",
  "message": "요청한 사이트/스코프에 대한 이용 권한이 없습니다",
  "data": null
}
검증 실패 — HTTP 200 인데 실패http
HTTP/1.1 200 OK
Content-Type: application/json
X-Partner-Server-Time: 1755400123

{
  "code": "400",
  "message": "잘못된 입력입니다 (isPeriodValid: to 는 from 이후여야 하고, 조회 기간은 180일 이하여야 합니다)",
  "data": null
}
Limits · Billing

한도 · 토큰 · 과금

3층 제한 — 축이 서로 다릅니다#

셋은 서로 다른 축에 걸립니다. 하나만 보고 "우리는 해당 없다"고 결론 내리지 마십시오.

제한버킷 축기본 상태한도초과 시저장소 장애 시
인증 실패 스로틀source IP상시 ON (기본값 그대로 동작)60초 창에 인증 실패 30건429 PT009 + Retry-After. 창이 지나면 자동 해제(잠금 아님)fail-OPEN(Redis) — 저장소 장애 시 통과시킵니다. 지키는 것이 비밀이 아니라 CPU·로그 볼륨이기 때문입니다
초당 버스트client_idARMED-BUT-UNSET — 기제는 있으나 값이 설정된 클라이언트가 없습니다(전역 기본값도 0 = 무제한)클라이언트별 burst_rps (설정된 경우에만)429 PT009 (Retry-After 없음)fail-OPEN(Redis)
월 토큰 쿼터client_id × KST yyyyMMfail-CLOSED — 미설정은 한도 0 으로 간주되어 첫 호출부터 402 입니다(전역 기본 시드 없음)월 limit_units (used + cost ≤ limit)402 PT003. 서빙 전 하드컷이라 데이터가 나가지 않습니다fail-CLOSED(Postgres) — 미터링 장애 시 503 이고 서빙하지 않습니다

게이트 순서 ⓪ ~ ⑨#

어느 게이트가 먼저 걸리는지를 알면 "왜 402 가 아니라 403 이 났나" 같은 질문이 스스로 풀립니다.

  • ⓪메서드 게이트(비-POST 거부, OPTIONS 만 예외) → 인증 실패 스로틀 PT009
  • ①경로에서 product_scope 해소(알 수 없는 경로면 거부) PT002
  • ②크리덴셜 검증. 내부 순서도 고정입니다: 서명 형식·시각(I/O 0) → 본문 상한 256KB → 본문 다이제스트 → 클라이언트 조회 → auth_mode 분기 → nonce 소모(맨 마지막). ★nonce 를 먼저 태우면 미인증 공격자가 정상 파트너의 nonce 를 선점해 그 파트너를 401 로 만드는 가용성 공격이 성립합니다 PT001
  • ③siteId 로 소유 테넌트 해소 + 그 테넌트의 grant 조회(사이트 미존재·비활성·grant 0건 모두 동일 응답) PT002
  • ④대상 테넌트 활성 확인 PT002
  • ⑤상품 토큰 단가 해소(단가 행 없음 = 서빙 불가) PT004
  • ⑥초당 버스트 게이트(클라이언트에 값이 설정된 경우에만) PT009
  • ⑦reserve — 월 카운터에 원자적 조건부 증가(선차감·하드컷) PT003
  • ⑧서빙 — 컨트롤러가 PARTNER_SCOPE_* authority 로 한 번 더 게이팅된 상태에서 실행
  • ⑨settle — 도메인 성공 플래그로 확정(성공: 원장 INSERT / 실패·취소·중복: 예약 해제)

토큰 미터링#

  • 1 토큰 = 1 cost_unit. 호출 비용은 상품의 cost_units 이며, 단가 이력은 append-only 입니다(리프라이스는 UPDATE 가 아니라 새 INSERT). v1 은 call-flat — 결과 행 수와 무관하게 서빙 전에 비용이 확정됩니다.
  • 2단계 미터링: reserve → settle. 서빙 전에 월 카운터에 원자적 조건부 증가(used + cost ≤ limit)로 예약하고, 초과면 402 로 끊습니다. 서빙 후에는 도메인-성공 플래그로 정산합니다 — 성공이면 멱등 원장 INSERT 로 과금이 확정되고, 실패·취소면 예약을 해제해 과금되지 않습니다. 중복 키 재호출도 예약을 해제해 1회만 과금되며, 응답은 매번 새로 계산됩니다(응답 캐시 없음).
  • 월 쿼터는 fail-CLOSED. 월 한도가 설정되지 않으면 0 으로 간주되어 즉시 402 입니다 — 전역 기본 시드가 없습니다. 월 키는 KST 기준 yyyyMM 입니다.
  • API 과금 테이블은 웹 티어와 물리적으로 분리됩니다. 사용량 테이블에는 tenant_key 컬럼이 없습니다 — client_id 축 과금이며, 웹 구독(테넌트 축)과 공유 카운터·공유 행·어느 방향으로도 스필오버가 없습니다.
  • Idempotency-Key 는 지출을 제한하지 데이터 소비량을 제한하지 않습니다. 같은 키를 반복해도 토큰이 더 나가지 않으므로, 월 한도가 "같은 리포트를 몇 번 다시 받을 수 있는가"를 제한하지는 못합니다. 그 축의 제어 수단은 초당 버스트 상한인데 현재 값이 설정된 클라이언트가 없습니다.

남은 토큰은 응답 헤더 X-Quota-Remaining 으로 매 호출 확인할 수 있습니다(402 응답에도 실립니다). 헤더별 조건과 값이 없을 때의 의미는 「응답 헤더」 절에 정리해 두었습니다. 응답 헤더 절로 →

스코프별 토큰 단가와 티어 호출 상한은 요금 페이지에 있습니다. 각 엔드포인트의 cost 는 위 레퍼런스 블록에도 표시됩니다.

무과금 조건 — 전 실패 경로#

상황처리 · 순 차감
R1도메인 실패(성공 플래그 미설정 — 티어 미달·소유 검증 실패·무효 커서 등)예약 해제 → 순 차감 0
R2취소·다운스트림 예외로 정산에 도달하지 못함종료 훅에서 예약 해제, 이중 보상은 플래그로 차단 → 순 차감 0
R3같은 멱등키의 중복 호출(서빙은 정상 성공)원장이 1행으로 접히고 예약분 반환 → 그 호출의 순 차감 0 (재서빙은 무료, 데이터는 새로 계산됨)
R4버스트 429reserve 이전 게이트라 예약 자체가 없음 → 0
R5402(한도 초과) · 503(미터링 장애)예약 미성립 → 0
R6신규 원장 INSERT 성공예약분 유지 = 확정 과금(이 경우에만 토큰이 나갑니다)
배선됨 · 기본 비활성 (INERT)ARMED-BUT-UNSET· burst 게이트 한정

₩ 토큰 단가(partner_token_price 돈다리), 선불 지갑(partner_token_wallet), 과금 모드 스위치(partner_client.billing_mode: monthly / prepaid), 버스트 리미터(초당 429 PARTNER_RATE_LIMITED)는 모두 빌드·배선되어 있으나 기본값은 비활성입니다. partner_token_price 의 ₩ 값은 전부 NULL(INERT)이라 CEO GO 게이트 전까지 실청구는 0입니다. 429 는 클라이언트에 burst_rps 가 설정된 경우에만 발생합니다 — 미설정이면 버스트 게이트를 건너뜁니다. 선불 지갑·prepaid 모드는 지갑 충전 후 billing_mode 전환 시 동작합니다.

★위 "미설정이면 게이트를 건너뜁니다"는 초당 버스트 한정입니다. 인증 실패 스로틀(60초 30건, 출발지 IP 축)은 기본 ON 이고, 월 토큰 쿼터는 fail-CLOSED 로 실제 집행됩니다 — 티어 게이팅과 cost_units 차감은 지금도 실동작합니다. ₩ 실청구가 없다는 것이 "제한이 없다"는 뜻이 아닙니다.

Authentication · PSR1

요청 서명 PSR1 — 심화

사용 가능

현재 상태#

아래 스펙은 API 서버에 배선되어 동작합니다. 서명 검증·nonce 1회성·시각 창·인증 실패 스로틀·X-Partner-Server-Time 응답 헤더가 모두 실동작 경로입니다. 아래 골든 테스트 벡터는 네트워크 없이 자기 구현을 검증합니다.

다만 자동으로 켜지지는 않습니다. 인증 모드는 크리덴셜 한 건 단위로 정해집니다 — 기존에 발급된 크리덴셜은 기본값이 시크릿 모드라 오늘 동작이 그대로 보존되고(서명 헤더를 보내도 무시됩니다), 새로 발급되는 크리덴셜은 기본값이 서명 모드로 서명키(psk_…)를 발급 응답에서 1회 받습니다.

이미 시크릿으로 통합하신 파트너가 서명으로 넘어오려면 담당자에게 요청해 주십시오 — 서명키를 같은 클라이언트 UUID 위에 발급해 드린 뒤 병존 모드를 거쳐 전환합니다. 키 교체(rotate)로 전환하지 않습니다.

별도 DB · 캐시 · 토큰 스토어가 필요합니까? — 아니오. 전혀 필요 없습니다.

서명은 매 요청 새로 계산되는 순수 함수입니다: f(서명키, 현재시각, 난수, 요청내용) → 서명. 인스턴스 간 공유할 상태가 원리적으로 없습니다.

서버 100대, 리전 3개, 오토스케일 0→50 파드, 서버리스(Lambda/Cloud Run) — 어느 경우에도 조율할 것이 0입니다. 토큰 캐시 DB, 갱신 스케줄러, 분산락, 리더 선출, 리전 복제, 스티키 세션, 고정 egress IP, 인증서 배포 — 전부 불필요합니다.

필요한 것은 3가지뿐입니다: ① 서명키 1개(env·시크릿매니저 — 지금 X-Partner-Secret 을 두던 그 자리) ② NTP 로 맞춰진 시계(허용 오차 ±5분, 클라우드 VM·컨테이너 기본값이 충족) ③ CSPRNG(난수, 전 언어 표준 라이브러리).

정규화 문자열 — 정확히 8줄, 개행(LF)으로 조인, 끝 개행 없음#

정규화 문자열 실물이 값은 바꾸지 마십시오
v1
POST
/partner/v1/keyword-budget/summary
3f2a9c1e-7b04-4d2a-9f11-0c8e5a6b1d33
1755400123
9f3c1a7e5b2d80463b1c7e5a9d204f68
bk-2026-08-17-0042
dc512c9616a558959a8983693a7da2fe0ead76e1a596cf4c71147cdb0b67ee36
줄무엇이 줄이 막는 것
1스킴 버전 (고정 문자열 v1)
v1
버전 다운그레이드 — 버전이 헤더에만 있으면 갈아끼울 수 있다
2HTTP 메서드 (항상 POST)
POST
메서드 변조
3요청 경로 (쿼리스트링 제외)
/partner/v1/keyword-budget/summary
스코프 상승 — market(1토큰) 서명을 keyword-budget(13토큰)에 재사용 불가
4X-Partner-Client 값
3f2a9c1e-…-0c8e5a6b1d33
헤더의 client 와 서명 주체 불일치
5X-Partner-Timestamp 값
1755400123
무기한 재생
6X-Partner-Nonce 값
9f3c1a7e5b2d80463b1c7e5a9d204f68
±5분 창 안에서의 재생
7Idempotency-Key 값 — ★미전송이면 빈 문자열(빈 줄)
bk-2026-08-17-0042
중간자가 멱등키를 갈아끼워 과금 중복 방지를 무력화하는 것
8요청 본문 원바이트의 sha256, 소문자 hex 64자
dc512c96…67ee36
본문 변조 — siteId 를 바꿔 다른 테넌트를 조회하는 것
  • 7번째 줄이 비어 있어도 줄은 항상 존재합니다. Idempotency-Key 를 보내지 않으면 그 자리는 빈 문자열이 되어 개행이 연속합니다. 이 줄을 통째로 빼면 8줄이 7줄이 되어 서명이 깨집니다 — 통합 실패 1위 지점입니다. 골든 벡터의 두 서명이 다른 이유가 바로 이 줄입니다.
  • 8번째 줄은 절대 비지 않습니다. 본문이 비어 있어도 빈 문자열의 sha256 상수가 들어갑니다(값은 아래 골든 테스트 벡터 블록 마지막 줄에 있습니다). 마지막 줄이 항상 채워지므로 "끝 개행 없음" 규칙과 충돌하지 않습니다.
  • 본문 다이제스트는 전송할 바이트 그대로의 sha256 입니다. 한글 키워드는 UTF-8 바이트로 해시하십시오 — 문자열이 아니라 바이트입니다.
  • 헤더 정규화(정렬·소문자화·SignedHeaders 목록)는 없습니다. 서명하는 값은 이 8줄이 전부이며 그 외 어떤 헤더도 서명 대상이 아닙니다.
  • 쿼리스트링은 서명 대상이 아니며 서버가 무시합니다 — APM·프록시가 ?trace_id= 를 붙여도 401 이 나지 않습니다(전 엔드포인트가 POST 이고 쿼리 파라미터를 하나도 읽지 않습니다).
  • HMAC 키는 발급받은 psk_… 문자열의 UTF-8 바이트 그대로입니다. base64 나 hex 로 디코딩하지 마십시오. psk_ 접두를 떼지도 마십시오 — 접두를 포함한 문자열 전체가 키입니다.
서명식
signature = "v1=" + base64( HMAC-SHA256( key = UTF8(서명키 문자열 전체), msg = UTF8(정규화문자열) ) )

골든 테스트 벡터 — 네트워크 없이 자기 구현을 검증하십시오#

이 값들을 넣고 아래 서명이 그대로 나오면 구현이 맞습니다. 서버에 붙기 전에 이것부터 통과시키십시오.

골든 테스트 벡터이 값은 바꾸지 마십시오
# 골든 테스트 벡터 — 아래 입력이면 아래 서명이 나와야 한다(언어 무관)
서명키(psk_)   psk_4f8a1c93b7e25d06a1f43c88e91b7d520c6a3f14e8b90d27
clientId       3f2a9c1e-7b04-4d2a-9f11-0c8e5a6b1d33
timestamp      1755400123
nonce          9f3c1a7e5b2d80463b1c7e5a9d204f68
path           /partner/v1/keyword-budget/summary
body(원바이트)  {"siteId":"b91c7e2d-4a13-4f88-9c02-5d6e7f801234","from":"2026-08-01","to":"2026-08-16","keywords":["강남 필라테스"]}
sha256(body)   dc512c9616a558959a8983693a7da2fe0ead76e1a596cf4c71147cdb0b67ee36

Idempotency-Key = "bk-2026-08-17-0042"  → X-Partner-Signature: v1=NCUBRYO9/HhNutFD1VNaYqVgM1pfbrhqo6HXaNtImIc=
Idempotency-Key 미전송(7번째 줄 빈 줄)  → X-Partner-Signature: v1=RPUdATkOe2c+v3DVbEfnmcHZFZ5Kc6HCIQrkdnFt2xI=

# 두 서명이 다르다는 사실이 7번째 줄의 존재를 증명한다 — 줄 자체는 절대 사라지지 않는다.
# 빈 본문의 sha256 상수(본문 없는 요청을 만들 일이 있으면 이 값을 8번째 줄에 넣는다):
#   e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

★위 body 의 keywords 필드는 서명 대상 바이트를 고정하기 위해 벡터에 포함된 값이며, keyword-budget/summary 의 요청 스키마에는 없는 필드입니다(서버가 무시합니다). 실제 요청 본문은 siteId · from · to 세 필드입니다 — 벡터를 서명 검증용으로만 쓰고, 요청 스키마는 엔드포인트 레퍼런스를 보십시오.

같은 벡터의 raw HTTP 형태이 값은 바꾸지 마십시오http
POST /partner/v1/keyword-budget/summary HTTP/1.1
Host: api.statlane.kr
Content-Type: application/json
X-Partner-Client:    3f2a9c1e-7b04-4d2a-9f11-0c8e5a6b1d33
X-Partner-Timestamp: 1755400123
X-Partner-Nonce:     9f3c1a7e5b2d80463b1c7e5a9d204f68
X-Partner-Signature: v1=NCUBRYO9/HhNutFD1VNaYqVgM1pfbrhqo6HXaNtImIc=
Idempotency-Key:     bk-2026-08-17-0042

{"siteId":"b91c7e2d-4a13-4f88-9c02-5d6e7f801234","from":"2026-08-01","to":"2026-08-16","keywords":["강남 필라테스"]}

구현 샘플 — 복사해서 그대로 쓰십시오 (전부 의존성 0)#

네 예제 모두 표준 라이브러리만 사용하며, 위 골든 벡터로 동일한 서명을 산출하는 것을 확인했습니다. 환경변수 두 개(STATLANE_CLIENT_ID, STATLANE_SIGNING_KEY)만 채우면 동작합니다.

javascript
// Node.js 18+ · 의존성 0 (crypto + 내장 fetch)
const crypto = require('crypto');

const BASE = 'https://api.statlane.kr';
const CLIENT_ID   = process.env.STATLANE_CLIENT_ID;    // X-Partner-Client
const SIGNING_KEY = process.env.STATLANE_SIGNING_KEY;  // psk_… ★문자열 그대로. base64/hex 디코딩 금지

function signedHeaders(path, bodyStr, idemKey) {
  const ts     = Math.floor(Date.now() / 1000).toString();   // epoch 초
  const nonce  = crypto.randomBytes(16).toString('hex');     // hex 32자
  const digest = crypto.createHash('sha256')
    .update(Buffer.from(bodyStr, 'utf8')).digest('hex');     // 한글은 UTF-8 바이트로
  const canonical = [
    'v1', 'POST', path, CLIENT_ID, ts, nonce, idemKey || '', digest,
  ].join('\n');                                              // 8줄 · 끝 개행 없음
  const sig = crypto.createHmac('sha256', Buffer.from(SIGNING_KEY, 'utf8'))
    .update(Buffer.from(canonical, 'utf8')).digest('base64');
  const h = {
    'Content-Type': 'application/json',
    'X-Partner-Client': CLIENT_ID,
    'X-Partner-Timestamp': ts,
    'X-Partner-Nonce': nonce,
    'X-Partner-Signature': 'v1=' + sig,
  };
  if (idemKey) h['Idempotency-Key'] = idemKey;   // 보내면 7번째 줄에 이미 들어가 있다
  return h;
}

async function call(path, payload, idemKey) {
  // ★서명한 그 문자열을 그대로 보낸다. 객체를 넘기면 라이브러리가 재직렬화해 서명이 깨진다.
  const body = JSON.stringify(payload);
  const res = await fetch(BASE + path, {
    method: 'POST', headers: signedHeaders(path, body, idemKey), body,
  });
  if (res.status === 401) {
    // 401 은 사유를 알려주지 않는다. 코드보다 시계를 먼저 본다.
    // X-Partner-Server-Time 은 성공·실패 불문 모든 응답에 실린다(epoch 초).
    const serverTime = Number(res.headers.get('x-partner-server-time'));
    const skew = Math.floor(Date.now() / 1000) - serverTime;
    throw new Error('401 PT001 · 시각차 ' + skew + '초(허용 ±300). ' +
      '진단 순서는 문서 「401 자가 진단」 절 참조');
  }
  if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + (await res.text()));

  // ★도메인 실패는 HTTP 200 으로 온다 — 상태코드만 보면 기간 초과·티어 미달을 성공으로 삼킨다.
  //   envelope 은 { code, message, data } 이고 성공은 code === '200' 이다.
  const json = await res.json();
  if (json.code !== '200') throw new Error(json.code + ' ' + json.message);
  return json.data;
}

call('/partner/v1/keyword-budget/summary',
     { siteId: '<SITE_ID>', from: '2026-08-01', to: '2026-08-16' },
     'bk-2026-08-17-0042')
  .then((data) => console.log(data))
  .catch((e) => console.error(e.message));

세 가지 함정#

  1. 서명한 바이트를 그대로 보내십시오. 객체가 아니라 바이트를 해시합니다. 미들웨어의 JSON 재직렬화(키 순서·공백 변경), pretty-print, 자동 gzip 은 전부 서명을 깹니다. 압축 본문(Content-Encoding)은 지원하지 않습니다.
  2. 한글 본문은 UTF-8 바이트로 해시하십시오. 문자열이 아니라 바이트입니다. CP949·UTF-16 으로 인코딩하면 다이제스트가 달라집니다. 골든 벡터에 한글 키워드를 일부러 넣어 둔 이유가 이것입니다.
  3. Idempotency-Key 는 서명 대상입니다 — 서명 전에 확정하십시오. 서명 이후에 헤더를 주입하는 미들웨어·재시도 래퍼가 있으면 401 이 납니다. 이 헤더는 정규화 문자열 7번째 줄이며, 보내지 않으면 그 줄이 빈 줄로 남습니다(줄 자체는 사라지지 않습니다).

이 스킴이 막는 것과 막지 못하는 것#

재생(replay)은 ±5분 시각 창과 1회용 nonce 로 제한됩니다. nonce 저장소 정상 시 재생은 차단됩니다. 저장소 장애 시에는 차단이 아니라 무과금 재서빙으로 저하되며(원장 멱등), 어떤 경우에도 순 차감은 0 입니다. 요청 자체를 중복 제거하거나 이전 응답을 재생하지는 않습니다.

유출 지점legacy 공유 시크릿PSR1 서명
요청 1건 캡처 (프록시 로그 · APM 헤더 덤프 · 지원 티켓의 HAR/curl)시크릿 원문 획득 → 만료일까지 모든 엔드포인트·모든 사이트·무제한서명 1개 획득 → 정확히 그 (경로, 본문 바이트, 멱등키) 조합 1건, 5분 이내, nonce 저장소 정상 시 0회 재사용
파트너 앱 로그에 헤더 전량 기록전면 침해1회용이라 피해 없음
파트너 측 키 유출 (git 커밋 · .env 덤프 · CI 변수 · 파트너 서버 침해)전면 침해전면 침해 — 동일. 서명은 이 유출 유형을 전혀 막지 못합니다. 완화는 만료일·키 교체·월 상한뿐입니다

폐기 속도는 두 스킴이 같습니다 — 차단 수단은 크리덴셜 상태·만료일 갱신뿐이고, 서버 캐시가 없어 다음 요청부터 즉시 반영됩니다. 서명이 더 빨리 폐기된다고 말할 근거는 없습니다.

전환 — 한 순간도 끊기지 않습니다#

  1. 클라이언트 UUID 는 바뀌지 않습니다. 서명키는 기존 크리덴셜에 추가 발급되며, grant·월 쿼터·사용 원장·귀사 로그가 전부 그대로 유지됩니다. 키 교체(rotate)로 전환하지 않습니다 — 교체는 새 클라이언트 UUID 를 만들어 쿼터와 grant 가 따라오지 않으므로 401 → 403 → 402 3연쇄 절단이 납니다.
  2. 병존 기간 서명 헤더가 있으면 서명으로, 없으면 기존 시크릿으로 인증합니다. 준비되는 대로 갈아타고, 문제가 생기면 즉시 기존 헤더로 되돌리면 됩니다. 단 서명 헤더를 보냈는데 서명이 틀리면 시크릿이 맞아도 401 입니다(조용한 다운그레이드 방지). 병존 기간에 시크릿으로 인증한 응답에는 Deprecation: true 와 Sunset(RFC 8594) 헤더가 실리므로, 귀사 HTTP 툴링이 우리 이메일보다 먼저 알려줍니다.
  3. 되돌리기는 한 줄입니다. 문제가 생기면 그 크리덴셜의 인증 모드를 시크릿으로 되돌립니다 — 배포 롤백도 스키마 롤백도 필요 없고, 영향 범위가 그 파트너 한 곳입니다. 전역 스위치를 만들지 않는 이유가 이것입니다(영향 범위가 "전 파트너"가 되어 버립니다).
  4. 종료일은 파트너별로 합의해 정합니다. 전역 컷오버 날짜를 두지 않습니다. 합의 전에는 기존 방식이 종료되지 않습니다.
Troubleshooting

401 이 났을 때 — 자가 진단

401(PT001)은 사유를 알려주지 않습니다 — 의도된 설계입니다. 어느 검사에서 걸렸는지 응답에 실으면 서명 스킴에 대한 오라클을 주는 것이기 때문입니다. 사유는 서버 로그에만 남습니다. 대신 아래 순서로 스스로 좁힐 수 있습니다. 코드보다 시계를 먼저 보십시오 — 실제 통합 장애의 가장 흔한 원인입니다.

증상원인확인 방법에러
어제까지 되던 통합이 갑자기 전부 401. 코드는 안 바꿨다시계 오차 — 컨테이너/VM 시각이 ±300초를 벗어났다(NTP 미동기, 스냅샷 복원, 서버리스 콜드스타트 드리프트)모든 응답(성공·실패 불문)에 X-Partner-Server-Time(epoch 초)이 실립니다 — 그 값과 내 date +%s 를 뺍니다. |차이| > 300 이면 원인 확정 — 코드가 아니라 NTP 를 고치십시오PT001
첫 호출은 200, 즉시 같은 요청을 재전송하면 401nonce 재사용 — 재시도 래퍼가 헤더를 캐시해 그대로 다시 보냈다재시도 경로에서 X-Partner-Nonce 가 매번 달라지는지 로그로 확인. 재시도는 새 타임스탬프 + 새 nonce + 재서명이며 Idempotency-Key 만 유지한다PT001
로컬 curl 은 200 인데 애플리케이션에서만 401본문 직렬화 차이 — 프레임워크/미들웨어가 JSON 을 재직렬화했다(키 순서·공백·유니코드 이스케이프·pretty print) 또는 자동 gzip 이 붙었다전송 직전 바이트를 그대로 찍어 sha256 을 내고, 서명에 쓴 8번째 줄과 문자 단위로 비교한다. 문자열 하나를 만들어 그것을 해시하고 그 문자열을 그대로 보내는 형태로 바꾼다. Content-Encoding(압축 본문)은 지원하지 않는다PT001
엔드포인트를 바꾸자 401. 이전 엔드포인트는 계속 200경로 불일치 — 3번째 줄에 서명한 경로와 실제 요청 경로가 다르다(베이스 URL 포함, 끝 슬래시, 대소문자)3번째 줄은 호스트 없는 경로만이며 쿼리스트링은 제외한다. 쿼리스트링은 서버가 무시하므로 붙어도 401 의 원인이 아니다PT001
키를 정확히 붙여넣었는데 계속 401키 디코딩 실수 — psk_ 문자열을 base64/hex 로 디코딩해서 HMAC 키로 썼다HMAC 키는 psk_ 를 포함한 문자열 전체의 UTF-8 바이트다. 골든 벡터로 자기 구현을 돌려 v1=NCUBRYO9/… 가 나오는지 먼저 확인한다(네트워크 없이 검증 가능)PT001
재시도 래퍼를 붙인 뒤부터 401Idempotency-Key 를 서명 이후에 주입했다 — 이 헤더는 서명 대상 7번째 줄이다서명 전에 멱등키를 확정한다. 헤더를 나중에 붙이는 인터셉터/프록시가 있으면 제거하거나 서명 함수 안으로 옮긴다PT001
401 을 반복하다 갑자기 429 로 바뀐다인증 실패 스로틀 — 같은 출발지 IP 에서 60초 안에 인증 실패가 30건을 넘으면 잠시 제한합니다(서명 무차별 대입 방어)Retry-After 헤더만큼 기다리면 카운터가 소멸해 스스로 풀립니다. 잠금이 아닙니다. 그 사이에 골든 벡터로 서명 구현부터 오프라인 검증하십시오PT009
400 PT005 가 난다본문에 siteId 가 없거나 snake_case(site_id)로 보냈다siteId 는 camelCase UUID 필수입니다. ★이 검사는 크리덴셜 검증보다 앞에서 일어납니다 — 인증 통과 여부와 무관하게 본문 문제이면 이 코드가 먼저 나오므로, PT005 를 받았다고 해서 인증이 통과한 것은 아닙니다PT005
HTTP 200 인데 기대한 데이터가 없다도메인 실패 또는 봉인. 검증 실패·티어 미달은 HTTP 200 + envelope code 가 "200" 이 아닌 값으로 나가고, k-익명 봉인은 code "200" 인 채로 해당 블록만 null + notice 로 나간다먼저 envelope 의 code 가 "200" 인지 봅니다 — 아니면 message 에 사유가 있습니다. code 가 "200" 인데 값이 null 이면 notice 를 읽으십시오(봉인은 장애가 아니라 정상 응답입니다)VALIDATION

그래도 좁혀지지 않으면 — 실패한 요청의 정규화 문자열 8줄을 그대로 출력해 문의에 첨부하십시오(★서명키는 절대 보내지 마십시오). 8줄만 있으면 어느 줄이 어긋났는지 우리 쪽에서 특정할 수 있습니다. 401 이 아니라 403(PT002)이면 인증은 통과한 것이며 grant 문제입니다 — 에러 카탈로그를 보십시오.

Versioning

버저닝 · 변경 이력

세 종류의 버전이 있고 서로 독립적으로 움직입니다. 어느 것을 감시해야 하는지부터 가르십시오.

버전어디에무엇의 버전인가
경로 /v1URL — /partner/v1/…제품 표면(엔드포인트 집합·필드 계약)의 버전. 아래 breaking 정의에 해당하는 변경이 필요할 때만 올라갑니다.
서명 v1=X-Partner-Signature 접두 · 정규화 문자열 1번째 줄서명 알고리즘 슬롯입니다. 경로 버전과 무관하게 움직이며, 지금 채워져 있는 값은 HMAC-SHA256 하나뿐입니다. ★비대칭(Ed25519 등) 모드는 만들지 않았습니다 — 슬롯이 비어 있다는 것이 곧 제공한다는 뜻이 아닙니다.
label.methodVersion응답 본문의 label산출 방법의 버전(예: conversions.summary.v1). 필드 셰이프가 그대로여도 계산 방식이 바뀌면 이 값이 올라갑니다 — 시계열을 이어 붙이신다면 실제로 감시해야 할 것은 경로 버전이 아니라 이 값입니다.
  • 응답 필드 추가는 breaking 이 아닙니다. 모르는 필드를 만나면 무시하도록 파서를 작성해 주십시오(엄격 모드 역직렬화는 필드 추가만으로 깨집니다).
  • breaking 으로 보는 것: 기존 필드의 삭제·이름 변경·타입 변경, 엔드포인트 경로 변경, 필수 요청 필드 추가, 기존 값 도메인에서 값이 사라지는 것. 이 경우 새 경로 버전으로 나가며 기존 버전을 예고 없이 끊지 않습니다.
  • nullable 조건이 넓어지는 것(더 자주 null 이 되는 것)은 breaking 으로 보지 않습니다 — 필드는 처음부터 nullable 로 계약되어 있고, null 을 0 으로 치환해 두신 구현이 있다면 그쪽이 이미 사실과 어긋나 있습니다.
  • ★Deprecation / Sunset 헤더는 인증 스킴 전용입니다(legacy 시크릿 경로). API 버전의 종료 통지가 아니므로, 그 헤더가 없다고 해서 이 API 버전이 영원하다는 뜻이 아니고 있다고 해서 API 가 끝나는 것도 아닙니다.
  • 토큰 단가는 append-only 이력으로 관리됩니다 — 단가 변경은 새 행 추가이고, 변경 시 사전에 알려 드립니다.

파트너 대면 변경 이력#

날짜종류변경
2026-08-18clarification응답 envelope 표기를 코드 실측값으로 정정했습니다 — { code, message, data } 이며 성공은 code="200" 입니다. 이전 문서가 적어 온 { success, data, error } 는 서버에 존재한 적이 없습니다. 서버 동작은 바뀌지 않았고 문서가 틀렸던 것입니다.
2026-08-17additivePSR1 요청 서명(X-Partner-Signature · Timestamp · Nonce)이 배선되어 동작합니다. 신규 발급 크리덴셜의 기본 인증 모드가 서명이 되었고, 기존 크리덴셜은 기본값이 시크릿이라 오늘 동작이 그대로 보존됩니다(행 단위 전환).
2026-08-17clarificationIdempotency-Key 의 원장 키 산출이 절단 방식에서 해시 방식으로 바뀌어 길이 제한이 사라졌습니다. 이전에는 앞부분이 같은 두 키가 한 건으로 접힐 수 있었습니다(그 시점에 발급된 크리덴셜이 없어 영향받은 파트너는 없습니다).
2026-08-17additivegrant 축이 사이트 단위에서 테넌트 단위로 바뀌었습니다. 이제 grant 는 그 테넌트의 모든 사이트(현재·미래)를 덮으므로, 고객이 사이트를 추가해도 새 grant 없이 조회할 수 있습니다.
2026-08-17additive인증 실패 스로틀(출발지 IP 축, 60초 30건 초과 시 429 + Retry-After)이 인증 앞단에 추가되었습니다. 정상 트래픽에는 영향이 없습니다.
Support

문의 · 약관

파트너 크리덴셜 발급·grant 추가·월 한도 증액·서명 전환은 전부 문의가 유일한 실경로입니다. 401 로 문의하실 때는 실패한 요청의 정규화 문자열 8줄을 첨부해 주십시오 — 서명키는 절대 보내지 마십시오.

웹 구독은 API 토큰 0개를, API 크리덴셜은 대시보드 접근 0을 포함합니다 — 두 축은 서로 교차하지 않습니다. 파트너 데이터 API 는 구독과 무관하게 전용 크리덴셜로만 발급합니다. (X-API-Key 는 별개 축 = 로그인 사용자 개인 토큰으로, 이 API 를 인증하지 않습니다.)