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에 들어갑니다.
{"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 아래의 유일한 공개 엔드포인트입니다.
{"username":"admin","password":"…"}{"token":"eyJhbGciOiJIUzI1NiJ9…","tokenType":"Bearer","expiresInSeconds":3600}에러. 잘못된 사용자명, 잘못된 비밀번호, 그리고 설정되지 않은 콘솔이 모두 401 invalid credentials 입니다 — 세 경우를 구분할 수 없게 한 것이 의도입니다. 이 출처 IP 가 제한에 걸린 뒤에는 429 입니다.
풀 상태
GET/api/pools/resources
KPI 요약과, 테넌트의 풀이 알고 있는 리소스마다 한 행 — 등록된 것, 차단 목록에 오른 것, 셀에서만 보인 것 모두 포함합니다. 행은 kind 다음 value 순으로 정렬됩니다.
{
"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}
리소스 하나를 컨텍스트별 셀로 펼쳐서, 컨텍스트 순으로 정렬해 돌려줍니다.
| 경로 파라미터 | 값 |
|---|---|
kind | PROXY · ACCOUNT · SESSION (대소문자 구분 없음) |
value | 리소스 값, URL 인코딩해서 넣습니다. |
{
"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 시간 차트가 그리는 데이터입니다.
| 쿼리 파라미터 | 기본값 | 설명 |
|---|---|---|
hours | 24 | 얼마나 과거까지 읽을지. [1, 720](30 일) 범위로 잘리므로 범위를 벗어난 값은 거절되는 대신 조용히 보정됩니다 — 그리고 어떤 호출자도 무한 스캔을 유발할 수 없습니다. |
{
"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 를 돌려줍니다.
| 쿼리 파라미터 | 기본값 | 설명 |
|---|---|---|
permanent | false | true 면 만료 없이 차단하고, 명시적 해제로만 풀립니다. |
seconds | 3600 | 임시 차단의 TTL. permanent=true 일 때는 무시되고, 0 이하 값은 기본값으로 대체됩니다. |
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 에 안전한 값. 없으면 "최신부터", 있으면 그 커서 바로 이전(더 오래된) 페이지를 뜻합니다. |
limit | 50 | 페이지 크기. [1, 500] 로 잘립니다. |
{
"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 | 원장의 전체 순서. 페이지 안에서도, 페이지를 넘어서도 엄격히 감소합니다. |
eventType | RESOURCE_LEASED · LEASE_RELEASED · RESOURCE_COOLED · RESOURCE_RECOVERED · RESOURCE_BLOCKLISTED · RESOURCE_UNBLOCKED |
context | 셀의 컨텍스트. 차단 목록 변경처럼 리소스 단위 이벤트에서는 null 입니다. |
until | 유형에 따라 쿨다운 종료·리스 만료·차단 만료 시각. 유형에 기한이 없거나 차단이 영구이면 null 입니다. |
cause | RESOURCE_COOLED 를 일으킨 FailureType. 그 밖에는 null. |
nextCursor | 다음(더 오래된) 페이지를 위해 cursor 로 되돌려주는 값. null 이면 마지막 페이지였다는 뜻입니다. |
에러. 이 엔드포인트에서 받은 것이 아닌 커서를 주면 400 invalid cursor 입니다. 커서를 직접 만들지 마세요 — 인코딩은 계약의 일부가 아닙니다.
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 기준입니다.
{
"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 이고, 이 검사는 존재 확인보다 먼저 돌아서 응답이 다른 테넌트의 존재를 드러낼 수 없습니다. 존재하지 않는 tenantId 는 404 tenant not found 로 답합니다. 저장· 교체·폐기의 의미는 인증에 있습니다.
POST/api/tenants/{tenantId}/api-keys
데이터플레인 API 키를 발급합니다. 201 Created 를 돌려줍니다. 본문은 선택 사항이며, 생략하면 레이블 없는 키가 됩니다.
{"label":"worker-01"}{
"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 이 아니면 그 키로는 더 이상 인증되지 않습니다.
[
{"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 | 요청 → 응답 | 설명 |
|---|---|---|
Register | ResourceId → 빈 응답 | 멱등. 리소스를 선택 대상으로 만듭니다. |
Acquire | Context → granted, lease | 리스 없는 granted: false 는 에러가 아니라 정상적인 답입니다. |
Report | ResourceId, Context, Outcome → 빈 응답 | 평판을 움직이는 유일한 호출이고, 셀을 만드는 유일한 호출입니다. |
Renew | LeaseHandle → renewed, lease | 차단 목록에 오른 리소스는 거절합니다 — 그 리스는 TTL 에서 끝납니다. |
Release | LeaseHandle → released | 현재 보유자의 펜싱 토큰만 반납할 수 있습니다. |
SubscribeEvents | 빈 요청 → stream PoolEvent | 실시간이고 테넌트 범위입니다. 영구 저장 쪽 대응물은 GET /api/events 입니다. |
gRPC 상태 코드
| 상태 | 이럴 때 |
|---|---|
UNAUTHENTICATED | 키가 없거나 모르는 키이거나 폐기된 키, 또는 그 키의 테넌트가 활성이 아님. |
UNAVAILABLE | 자격증명을 확인할 수 없었음(저장소에 닿지 않음). 재시도 대상입니다. |
INVALID_ARGUMENT | 잘못된 요청 — kind 가 비어 있거나, 리소스 값이 공백이거나, 컨텍스트가 없는 경우. |
RESOURCE_EXHAUSTED | 호출이 서비스 전체 예산을 넘겨 새 풀 상태를 만들려는 경우(Register 의 새 리소스, Report 의 새 셀). 기존 상태만 건드리는 호출은 영향을 받지 않습니다 — 자주 묻는 질문을 보세요. |