REST API 레퍼런스

컨트롤플레인의 모든 엔드포인트 — 메서드·경로·파라미터·요청과 응답 본문, 그리고 각각이 돌려주는 에러.

공통 규칙

  • 기준 주소https://<your-console-host>/api, 직접 띄운 스택이라면 http://localhost:8080/api. 컨트롤플레인은 대시보드와 같은 오리진에서 서빙되므로 브라우저 도구에 CORS 설정이 필요하지 않습니다.
  • 인증POST /api/auth/login 을 제외한 모든 엔드포인트가 Authorization: Bearer <jwt> 를 요구합니다. 여기서는 API 키가 통하지 않습니다. 인증을 보세요.
  • 테넌트 — 범위가 있는 조회는 파라미터가 아니라 토큰의 테넌트를 씁니다. 다른 테넌트의 데이터를 요청할 방법이 없습니다.
  • 시각 — UTC 기준 ISO-8601 순간값. null 은 "설정되지 않음"을 뜻합니다(쿨다운 없음, 만료 없음, 폐기되지 않음).
  • 에러 — RFC 7807 application/problem+json. 정리된 사유는 detail 에 들어갑니다.
jsonerror shape
{"type":"about:blank","title":"Not Found","status":404,"detail":"resource not found"}
상태이럴 때
400엔드포인트가 직접 검증하는 잘못된 입력 — 모르는 리소스 kind, 손상된 cursor.
401토큰이 없거나 형식이 잘못됐거나 만료됨, 또는 로그인 자격증명이 틀림. 다시 로그인하세요.
403토큰이 테넌트에 묶여 있지 않거나, 다른 테넌트를 향하거나, 그 테넌트가 정지·삭제됨.
404지정한 대상이 이 테넌트에는 존재하지 않음.
409쓰기가 기존 상태와 충돌함.
429로그인 제한에 걸림. Retry-After 를 지키세요.

인증

POST/api/auth/login

관리자 자격증명을 컨트롤플레인 JWT 로 교환합니다. /api 아래의 유일한 공개 엔드포인트입니다.

jsonrequest
{"username":"admin","password":"…"}
json200 OK
{"token":"eyJhbGciOiJIUzI1NiJ9…","tokenType":"Bearer","expiresInSeconds":3600}

에러. 잘못된 사용자명, 잘못된 비밀번호, 그리고 설정되지 않은 콘솔이 모두 401 invalid credentials 입니다 — 세 경우를 구분할 수 없게 한 것이 의도입니다. 이 출처 IP 가 제한에 걸린 뒤에는 429 입니다.

풀 상태

GET/api/pools/resources

KPI 요약과, 테넌트의 풀이 알고 있는 리소스마다 한 행 — 등록된 것, 차단 목록에 오른 것, 셀에서만 보인 것 모두 포함합니다. 행은 kind 다음 value 순으로 정렬됩니다.

json200 OK
{
  "summary": {
    "registered": 42,
    "blocklisted": 1,
    "totalCells": 96,
    "cellsByState": {"HEALTHY": 81, "COOLING": 12, "RECOVERING": 3, "BLOCKLISTED": 0}
  },
  "resources": [
    {
      "kind": "PROXY",
      "value": "proxy-1.example.net:8080",
      "registered": true,
      "blocked": false,
      "blockedUntil": null,
      "blockPermanent": false,
      "contexts": 2,
      "state": "COOLING",
      "score": -34.0,
      "recentWindow": [true, true, false, false]
    }
  ]
}

한 행은 그 리소스의 셀들을 대표값으로 집계한 것이고, 규칙은 "가장 나쁜 컨텍스트를 드러낸다" 입니다 — 운영자가 먼저 봐야 하는 것이 그것이기 때문입니다.

  • state — 셀 상태 중 심각도가 가장 높은 것(BLOCKLISTED > COOLING > RECOVERING > HEALTHY). 리소스 자체가 차단됐으면 곧바로 BLOCKLISTED, 셀이 아직 없으면 HEALTHY.
  • score — 셀들 중 가장 낮은 점수. 셀이 없으면 null.
  • recentWindow점수가 가장 낮은 셀의 윈도를 성공 플래그로(오래된 것 → 최신 순) 보여 줍니다. 셀이 없으면 빈 배열입니다. 심각도와 최저 점수는 따로 계산되므로, 한 행이 COOLING 이라고 표시하면서 점수를 끌어내리고 있는 다른 컨텍스트의 윈도를 보여 줄 수 있습니다.
  • contexts — 리소스가 가진 셀 수. 0 은 등록됐지만 보고된 적이 없다는 뜻입니다.
  • blockedUntil — 임시 차단의 만료 시각. 차단이 풀렸거나 영구 차단이면 null 이므로 blockPermanent 와 함께 읽어야 합니다.

GET/api/pools/resources/{kind}/{value}

리소스 하나를 컨텍스트별 셀로 펼쳐서, 컨텍스트 순으로 정렬해 돌려줍니다.

경로 파라미터
kindPROXY · ACCOUNT · SESSION (대소문자 구분 없음)
value리소스 값, URL 인코딩해서 넣습니다.
json200 OK
{
  "kind": "PROXY",
  "value": "proxy-1.example.net:8080",
  "registered": true,
  "blocked": false,
  "blockedUntil": null,
  "blockPermanent": false,
  "cells": [
    {
      "context": "checkout-us",
      "score": -34.0,
      "consecutiveFailures": 2,
      "consecutiveSuccesses": 0,
      "windowSize": 10,
      "state": "COOLING",
      "cooldownUntil": "2026-07-29T10:41:02Z",
      "updatedAt": "2026-07-29T09:41:02Z"
    },
    {
      "context": "search-eu",
      "score": 35.0,
      "consecutiveFailures": 0,
      "consecutiveSuccesses": 7,
      "windowSize": 10,
      "state": "HEALTHY",
      "cooldownUntil": null,
      "updatedAt": "2026-07-29T09:44:18Z"
    }
  ]
}

에러. 모르는 kind 이거나 값이 비었으면 400 invalid resource kind or value. 풀이 이 리소스를 한 번도 본 적이 없으면 — 즉 셀이 없고, 등록되지 않았고, 차단되지도 않았으면 — 404 resource not found.

GET/api/pools/resources/{kind}/{value}/score-history

리소스의 점수 곡선을 샘플링한 값이고, 컨텍스트마다 시간 오름차순 시리즈 하나입니다. 대시보드의 24 시간 차트가 그리는 데이터입니다.

쿼리 파라미터기본값설명
hours24얼마나 과거까지 읽을지. [1, 720](30 일) 범위로 잘리므로 범위를 벗어난 값은 거절되는 대신 조용히 보정됩니다 — 그리고 어떤 호출자도 무한 스캔을 유발할 수 없습니다.
json200 OK
{
  "contexts": [
    {"context": "checkout-us",
     "points": [{"at":"2026-07-29T08:00:00Z","score":15.0},
                {"at":"2026-07-29T08:01:00Z","score":-15.0}]},
    {"context": "search-eu",
     "points": [{"at":"2026-07-29T08:00:00Z","score":30.0}]}
  ]
}

점수는 보고마다 기록되는 것이 아니라 타이머로(1 분에 한 번) 샘플링되므로, 이 곡선은 결과 전체의 이력이 아니라 표본입니다. 모르는 리소스는 200 과 빈 contexts 배열로 답합니다 — 시리즈가 없는 것일 뿐입니다. 표본은 7 일간 보존됩니다. 자주 묻는 질문을 보세요.

POST/api/pools/resources/{kind}/{value}/block

리소스를 차단 목록에 올립니다 — 모든 컨텍스트에서 한 번에 선택에서 격리하는 운영자 개입입니다. 204 No Content 를 돌려줍니다.

쿼리 파라미터기본값설명
permanentfalsetrue 면 만료 없이 차단하고, 명시적 해제로만 풀립니다.
seconds3600임시 차단의 TTL. permanent=true 일 때는 무시되고, 0 이하 값은 기본값으로 대체됩니다.
bashtemporary and permanent
curl -sS -X POST "https://$RP_HOST/api/pools/resources/proxy/proxy-1/block?seconds=7200" \
  -H "Authorization: Bearer $RP_JWT"

curl -sS -X POST "https://$RP_HOST/api/pools/resources/proxy/proxy-1/block?permanent=true" \
  -H "Authorization: Bearer $RP_JWT"

차단은 RESOURCE_BLOCKLISTED 를 내보내므로 나머지 타임라인과 함께 감사 로그와 실시간 스트림에 남습니다. 이미 차단된 리소스를 다시 차단하면 기존 항목을 덮어씁니다. kind 나 값이 잘못되면 400 입니다.

DELETE/api/pools/resources/{kind}/{value}/block

리소스를 차단 목록에서 풀어 줍니다. 204 No Content 를 돌려주고, 실제로 차단돼 있었을 때만 RESOURCE_UNBLOCKED 를 내보냅니다 — 차단되지 않은 것을 해제하는 것은 유령 감사 기록을 남기는 대신 아무 일도 하지 않습니다. kind 나 값이 잘못되면 400 입니다.

해제는 평판을 초기화하지 않습니다. 리소스는 셀 상태를 그대로 들고 선택 대상으로 돌아오므로, 아직 쿨다운 중인 셀은 자기 컨텍스트에서 쿨다운이 끝날 때까지 계속 빠져 있습니다.

감사 이벤트

GET/api/events

테넌트의 감사 로그 한 페이지를 최신순으로, keyset(cursor) 페이지네이션으로 돌려줍니다 — 얼마나 과거로 내려갔든 한 페이지의 비용이 일정합니다.

쿼리 파라미터기본값설명
cursor불투명하고 URL 에 안전한 값. 없으면 "최신부터", 있으면 그 커서 바로 이전(더 오래된) 페이지를 뜻합니다.
limit50페이지 크기. [1, 500] 로 잘립니다.
json200 OK
{
  "events": [
    {"seq": 918, "eventType": "RESOURCE_COOLED", "resourceKind": "PROXY",
     "resourceValue": "proxy-1.example.net:8080", "context": "checkout-us",
     "occurredAt": "2026-07-29T09:41:02Z", "until": "2026-07-29T10:41:02Z", "cause": "BLOCKED"},
    {"seq": 917, "eventType": "RESOURCE_LEASED", "resourceKind": "PROXY",
     "resourceValue": "proxy-1.example.net:8080", "context": "checkout-us",
     "occurredAt": "2026-07-29T09:40:58Z", "until": "2026-07-29T09:41:28Z", "cause": null}
  ],
  "nextCursor": "OTE3"
}
필드의미
seq원장의 전체 순서. 페이지 안에서도, 페이지를 넘어서도 엄격히 감소합니다.
eventTypeRESOURCE_LEASED · LEASE_RELEASED · RESOURCE_COOLED · RESOURCE_RECOVERED · RESOURCE_BLOCKLISTED · RESOURCE_UNBLOCKED
context셀의 컨텍스트. 차단 목록 변경처럼 리소스 단위 이벤트에서는 null 입니다.
until유형에 따라 쿨다운 종료·리스 만료·차단 만료 시각. 유형에 기한이 없거나 차단이 영구이면 null 입니다.
causeRESOURCE_COOLED 를 일으킨 FailureType. 그 밖에는 null.
nextCursor다음(더 오래된) 페이지를 위해 cursor 로 되돌려주는 값. null 이면 마지막 페이지였다는 뜻입니다.

에러. 이 엔드포인트에서 받은 것이 아닌 커서를 주면 400 invalid cursor 입니다. 커서를 직접 만들지 마세요 — 인코딩은 계약의 일부가 아닙니다.

bashwalking the whole trail
cursor=""
while :; do
  page=$(curl -sS "https://$RP_HOST/api/events?limit=500&cursor=$cursor" \
           -H "Authorization: Bearer $RP_JWT")
  echo "$page" | jq -c '.events[]'
  cursor=$(echo "$page" | jq -r '.nextCursor // empty')
  [ -z "$cursor" ] && break
done

사용량

GET/api/usage

테넌트의 측정된 사용량 — 최근 30 일간 리스 발급 수, 이번 달(달력 기준) 합계, 그리고 가장 최근에 샘플링한 풀 크기입니다. 날짜는 UTC 기준입니다.

json200 OK
{
  "monthLeaseTotal": 128400,
  "poolSize": 42,
  "dailyLeases": [
    {"date": "2026-07-28", "count": 5120},
    {"date": "2026-07-29", "count": 3980}
  ]
}

리스는 발급된 시점에 집계되므로 granted: false 로 끝난 Acquire 는 세지 않습니다. 카운트는 메모리에 모아 타이머로(1 분에 한 번) 내려쓰므로, 오늘 숫자는 실시간 트래픽보다 조금 뒤처집니다. 활동이 없는 날은 0 으로 들어가는 대신 배열에서 아예 빠집니다.

API 키

세 엔드포인트 모두 tenantId 경로 파라미터를 받고, 그것은 토큰이 묶인 테넌트여야 합니다 — 아니면 403 forbidden 이고, 이 검사는 존재 확인보다 먼저 돌아서 응답이 다른 테넌트의 존재를 드러낼 수 없습니다. 존재하지 않는 tenantId404 tenant not found 로 답합니다. 저장· 교체·폐기의 의미는 인증에 있습니다.

POST/api/tenants/{tenantId}/api-keys

데이터플레인 API 키를 발급합니다. 201 Created 를 돌려줍니다. 본문은 선택 사항이며, 생략하면 레이블 없는 키가 됩니다.

jsonrequest (optional)
{"label":"worker-01"}
json201 Created
{
  "id": "5f1c2b40-…",
  "rawToken": "rp_9Q3xK7bT…",
  "label": "worker-01",
  "prefix": "rp_9Q3xK7bT",
  "createdAt": "2026-07-29T09:12:44Z"
}

GET/api/tenants/{tenantId}/api-keys

테넌트의 모든 키를 오래된 순으로 — 키 원문은 절대 없고, 비밀이 아닌 표시용 접두사만 들어 있습니다. revokedAt 이 null 이 아니면 그 키로는 더 이상 인증되지 않습니다.

json200 OK
[
  {"id":"5f1c2b40-…","label":"worker-01","prefix":"rp_9Q3xK7bT",
   "createdAt":"2026-07-29T09:12:44Z","revokedAt":null}
]

DELETE/api/tenants/{tenantId}/api-keys/{keyId}

활성 키를 id 로 폐기합니다. 204 No Content 를 돌려줍니다. 즉시 적용되므로 그 키로 보내는 다음 gRPC 호출부터 실패합니다.

에러. 404 api key not found 가 "모르는 id", "이미 폐기됨", "다른 테넌트의 키" 세 경우를 모두 덮으며, 구분하지 않습니다.

gRPC 데이터플레인 — 참고용

REST 는 아니지만 표면 전체를 한 곳에서 보기 위해 함께 적습니다. 서비스는 io.github.preagile.reputationpool.grpc.v1.ReputationAdvisor 이고, x-api-key 메타데이터로 인증하며, 앱 컨테이너의 9093 포트에서 서빙됩니다 — 127.0.0.1 에만 공개되므로 호스티드 호스트명이 아니라 직접 띄운 스택에서 닿습니다. 메시지 모양과 실행 가능한 호출 예제는 퀵스타트에 있습니다.

RPC요청 → 응답설명
RegisterResourceId → 빈 응답멱등. 리소스를 선택 대상으로 만듭니다.
AcquireContextgranted, lease리스 없는 granted: false 는 에러가 아니라 정상적인 답입니다.
ReportResourceId, Context, Outcome → 빈 응답평판을 움직이는 유일한 호출이고, 셀을 만드는 유일한 호출입니다.
RenewLeaseHandlerenewed, lease차단 목록에 오른 리소스는 거절합니다 — 그 리스는 TTL 에서 끝납니다.
ReleaseLeaseHandlereleased현재 보유자의 펜싱 토큰만 반납할 수 있습니다.
SubscribeEvents빈 요청 → stream PoolEvent실시간이고 테넌트 범위입니다. 영구 저장 쪽 대응물은 GET /api/events 입니다.

gRPC 상태 코드

상태이럴 때
UNAUTHENTICATED키가 없거나 모르는 키이거나 폐기된 키, 또는 그 키의 테넌트가 활성이 아님.
UNAVAILABLE자격증명을 확인할 수 없었음(저장소에 닿지 않음). 재시도 대상입니다.
INVALID_ARGUMENT잘못된 요청 — kind 가 비어 있거나, 리소스 값이 공백이거나, 컨텍스트가 없는 경우.
RESOURCE_EXHAUSTED호출이 서비스 전체 예산을 넘겨 새 풀 상태를 만들려는 경우(Register 의 새 리소스, Report 의 새 셀). 기존 상태만 건드리는 호출은 영향을 받지 않습니다 — 자주 묻는 질문을 보세요.