REST API reference
Every control-plane endpoint: method, path, parameters, request and response bodies, and the errors each one returns.
Conventions
- Base —
https://<your-console-host>/api, orhttp://localhost:8080/apiagainst a stack you started yourself. The control plane is served from the same origin as the dashboard, so browser tooling needs no CORS setup. - Auth — every endpoint except
POST /api/auth/loginrequiresAuthorization: Bearer <jwt>. API keys do not work here; see Authentication. - Tenant — scoped reads use the tenant on your token, never a parameter. There is no way to ask for another tenant's data.
- Times — ISO-8601 instants in UTC.
nullmeans "not set" (no cooldown, no expiry, not revoked). - Errors — RFC 7807
application/problem+json. The curated reason is indetail.
{"type":"about:blank","title":"Not Found","status":404,"detail":"resource not found"}| Status | When |
|---|---|
400 | Malformed input the endpoint validates itself — an unknown resource kind, a corrupt cursor. |
401 | Missing, malformed, or expired token; or wrong login credentials. Log in again. |
403 | The token is not bound to a tenant, targets another tenant, or its tenant is suspended/deleted. |
404 | The addressed thing does not exist for your tenant. |
409 | The write conflicts with existing state. |
429 | Login throttled. Honour Retry-After. |
Authentication
POST/api/auth/login
Exchanges admin credentials for a control-plane JWT. The only public endpoint under /api.
{"username":"admin","password":"…"}{"token":"eyJhbGciOiJIUzI1NiJ9…","tokenType":"Bearer","expiresInSeconds":3600}Errors. 401 invalid credentials for a wrong username, a wrong password, and an unconfigured console — the three are indistinguishable on purpose. 429 once this source IP has been throttled.
Pool state
GET/api/pools/resources
KPI summary plus one row per resource your tenant's pool knows — registered, blocklisted, or merely seen in a cell. Rows are ordered by kind then 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]
}
]
}A row aggregates the resource's cells into a representative rollup, and the rule is "surface the weakest context" — that is the one an operator needs to see first:
state— the worst-severity cell state (BLOCKLISTED>COOLING>RECOVERING>HEALTHY), orBLOCKLISTEDoutright if the resource is blocked, orHEALTHYif it has no cells yet.score— the lowest score across the cells;nullwhen there are none.recentWindow— success flags (oldest → newest) of the worst-scoring cell's window; an empty array when there are no cells. Severity and worst score are computed independently, so a row can readCOOLINGwhile showing the window of whichever context is dragging its score down.contexts— how many cells the resource holds.0means registered but never reported on.blockedUntil— the expiry of a temporary block;nullwhen unblocked or permanent, so read it together withblockPermanent.
GET/api/pools/resources/{kind}/{value}
One resource expanded into its per-context cells, sorted by context.
| Path param | Values |
|---|---|
kind | PROXY · ACCOUNT · SESSION (case-insensitive) |
value | The resource value, URL-encoded. |
{
"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"
}
]
}Errors. 400 invalid resource kind or value for an unknown kind or a blank value. 404 resource not found when the pool has never seen this resource — that is, it has no cells, is not registered, and is not blocked.
GET/api/pools/resources/{kind}/{value}/score-history
The resource's sampled score curve, one ascending-time series per context. This is what the dashboard's 24-hour chart draws.
| Query param | Default | Notes |
|---|---|---|
hours | 24 | How far back to read. Clamped to [1, 720] (30 days), so an out-of-range value is silently corrected rather than rejected — and no caller can trigger an unbounded scan. |
{
"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}]}
]
}Scores are sampled on a timer (once a minute), not written on every report, so the curve is a sampled view rather than a full history of outcomes. An unknown resource returns 200 with an empty contexts array — there is simply no series. Samples are retained for seven days; see FAQ.
POST/api/pools/resources/{kind}/{value}/block
Blocklists a resource — the operator override that isolates it from selection in every context at once. Returns 204 No Content.
| Query param | Default | Notes |
|---|---|---|
permanent | false | true blocks with no expiry, released only by an explicit unblock. |
seconds | 3600 | TTL of a temporary block. Ignored when permanent=true; a zero or negative value falls back to the default. |
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"The block emits RESOURCE_BLOCKLISTED, so it lands in the audit trail and the live stream with the rest of the timeline. Blocking an already-blocked resource replaces the entry. 400 for an invalid kind or value.
DELETE/api/pools/resources/{kind}/{value}/block
Releases a resource from the blocklist. Returns 204 No Content, and emits RESOURCE_UNBLOCKED only if it really was blocked — so unblocking something that is not blocked is a harmless no-op rather than a phantom audit entry. 400 for an invalid kind or value.
Releasing does not reset reputation. The resource returns to selection with its cells exactly as they were, so a cell that is still cooling stays benched for its own context until its cooldown expires.
Audit events
GET/api/events
One page of your tenant's audit trail, newest first, with keyset (cursor) pagination — the cost of a page is flat no matter how far back you have scrolled.
| Query param | Default | Notes |
|---|---|---|
cursor | — | Opaque, URL-safe. Absent means "start at the latest"; otherwise the page immediately older than the cursor. |
limit | 50 | Page size, clamped to [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"
}| Field | Meaning |
|---|---|
seq | The ledger's total order. Strictly decreasing within and across pages. |
eventType | RESOURCE_LEASED · LEASE_RELEASED · RESOURCE_COOLED · RESOURCE_RECOVERED · RESOURCE_BLOCKLISTED · RESOURCE_UNBLOCKED |
context | The cell's context, or null for resource-level events such as a blocklist change. |
until | Cooldown end, lease expiry, or block expiry depending on the type; null when the type has no deadline (or the block is permanent). |
cause | The FailureType behind a RESOURCE_COOLED; null otherwise. |
nextCursor | Pass it back as cursor for the next older page. null means this was the last page. |
Errors. 400 invalid cursor for a cursor you did not get from this endpoint. Do not construct cursors — the encoding is not part of the contract.
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
doneUsage
GET/api/usage
Your tenant's metered usage: the last 30 days of granted-lease counts, the current calendar month's total, and the most recently sampled pool size. Days are UTC.
{
"monthLeaseTotal": 128400,
"poolSize": 42,
"dailyLeases": [
{"date": "2026-07-28", "count": 5120},
{"date": "2026-07-29", "count": 3980}
]
}A lease is counted when it is granted, so an Acquire that returned granted: false does not count. Counts are accumulated in memory and flushed on a timer (once a minute), so the current day's number trails live traffic slightly. Days with no activity are absent from the array rather than present with a zero.
API keys
All three endpoints take a tenantId path parameter, and it must be the tenant your token is bound to — otherwise 403 forbidden, checked before existence so the response cannot reveal whether another tenant exists. A tenantId that does not exist answers 404 tenant not found. Storage, rotation, and revocation semantics are in Authentication.
POST/api/tenants/{tenantId}/api-keys
Mints a data-plane API key. Returns 201 Created. The body is optional; omit it for an unlabelled key.
{"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
All of the tenant's keys, oldest first — never key material, only the non-secret display prefix. A non-null revokedAt means the key no longer authenticates.
[
{"id":"5f1c2b40-…","label":"worker-01","prefix":"rp_9Q3xK7bT",
"createdAt":"2026-07-29T09:12:44Z","revokedAt":null}
]DELETE/api/tenants/{tenantId}/api-keys/{keyId}
Revokes an active key by its id. Returns 204 No Content. Effective immediately — the next gRPC call with that key fails.
Errors. 404 api key not found covers all three of "unknown id", "already revoked", and "belongs to another tenant", without distinguishing them.
gRPC data plane, for reference
Not REST, but listed here so the whole surface is in one place. Service io.github.preagile.reputationpool.grpc.v1.ReputationAdvisor, authenticated with x-api-keymetadata, served on port 9093 of the app container — published on 127.0.0.1 only, so you reach it from a stack you started rather than from a hosted hostname. Message shapes and runnable calls are in Quickstart.
| RPC | Request → response | Notes |
|---|---|---|
Register | ResourceId → empty | Idempotent. Makes the resource eligible for selection. |
Acquire | Context → granted, lease | granted: false with no lease is a normal answer, not an error. |
Report | ResourceId, Context, Outcome → empty | The only call that moves reputation, and the only one that creates a cell. |
Renew | LeaseHandle → renewed, lease | Refuses a blocklisted resource — the lease then lapses at its TTL. |
Release | LeaseHandle → released | Only the current holder's fencing token can release. |
SubscribeEvents | empty → stream PoolEvent | Live, tenant-scoped. The durable equivalent is GET /api/events. |
gRPC status codes
| Status | When |
|---|---|
UNAUTHENTICATED | Missing, unknown, or revoked key; or the key's tenant is not active. |
UNAVAILABLE | Credentials could not be checked (store unreachable). Retryable. |
INVALID_ARGUMENT | Malformed request — an unset kind, a blank resource value, a missing context. |
RESOURCE_EXHAUSTED | The call would create new pool state (a new resource on Register, a new cell on Report) past the service-wide budget. Calls that only touch existing state are never affected — see FAQ. |