Score API Reference (Signals)
Module: Signals
Base route: /api/v1/score
Source of truth: src/Signals/StellaOps.Signals/Score/Endpoints/ScoreEndpoints.cs, contracts in Score/Contracts/ScoreModels.cs + Score/Contracts/ScoreHistoryRecord.cs.
Scope note: this page documents the Signals unified-score API.
Scanner score replay endpoints are implemented separately at:
- primary:
/api/v1/scans/{scanId}/score/replay|bundle|verify|history- compatibility aliases:
/api/v1/score/{scanId}/replay|bundle|verify|historyThe Scanner replay surface uses a different contract (proof-bundle / Merkle root hash) and is authorized with
scanner:read(GET) /scanner:write(POST). Seesrc/Scanner/StellaOps.Scanner.WebService/Endpoints/ScoreReplayEndpoints.csanddocs/modules/scanner/architecture.md. The Signals endpoints documented below are NOT the same as the Scanner endpoints despite the overlapping/score/...path segment.
Overview
Audience: Backend integrators, auditors, and CLI/Web clients that compute, replay, verify, or explain unified trust scores through Router against Signals.
The score API exposes deterministic unified-trust-score computation, replay verification, weight-manifest lookup, and explanation payloads. All responses are tenant-scoped and wrapped in the score item/list envelope.
Authentication and tenant context
- Bearer token authentication is required.
- The endpoint group requires policy
signals.score.read, backed by canonical OAuth claimscore.read. Tenant context is enforced viaRequireTenant(). POST /api/v1/score/evaluateadditionally requires policysignals.score.evaluate, backed byscore.evaluate; because the group is read-protected, evaluation requires both claims.- Every other endpoint on this group — including
GET /{scoreId},GET /history,GET /weights*,GET /explain/{digest},GET /{scoreId}/replay, andPOST /verify— requires onlyscore.read. - Tenant context is resolved by
ScoreRequestContextResolverfrom the authenticated claim-bound tenant context. A missing/unresolvable tenant returns a deterministic tenant failure and request tenant values are compatibility assertions, not authority.
signals.score.read/signals.score.evaluateare ASP.NET policy names;score.read/score.evaluateare canonical claim values inStellaOpsScopesand Authority client/permission grants.
Response envelope
Single-item responses use ScoreItemResponse<T> and are serialized with the ASP.NET Core Web defaults (camelCase envelope keys):
{
"tenantId": "tenant-a",
"actorId": "user-1",
"dataAsOf": "2026-02-26T12:00:00Z",
"cached": true,
"cacheTtlSeconds": 300,
"item": { },
"isDemo": false
}
List responses use ScoreListResponse<T>:
{
"tenantId": "tenant-a",
"actorId": "user-1",
"dataAsOf": "2026-02-26T12:00:00Z",
"cached": false,
"cacheTtlSeconds": 0,
"items": [ ],
"count": 0,
"limit": null,
"offset": null,
"query": null,
"isDemo": false
}
The envelope keys (
tenantId,item,items,count, …) are camelCase, but the fields of the wrappeditem/itemspayloads use the explicitsnake_casenames declared by the contract DTOs’[JsonPropertyName]attributes (for examplescore_id,final_score,contract_version). The two casings co-exist on the wire by design.
Endpoints
POST /api/v1/score/evaluate
Computes a unified trust score (EWS computation combined with Determinization entropy) from provided signal inputs. Requires both score.read and score.evaluate. Emits a Signals audit event (AuditActions.Signals.EvaluateScore). Successful evaluations are persisted to the score-history store.
Query parameters:
include_delta(bool, optional) — overridesoptions.include_deltafor delta-if-present calculations.
Request body (ScoreEvaluateRequest, snake_case):
sbom_ref(string) — SBOM reference (OCI digest or URL).cve_id(string) — CVE identifier for direct scoring.purl(string) — package URL for component identification.cvss_vector(string) — CVSS vector string.vex_refs(string[]) — VEX document references.rekor_receipts(string[]) — Rekor receipt data.runtime_witnesses(object[]) —{ type, data, observed_at? }runtime observations.signals(object) — direct signal inputs (reachability,runtime,backport,exploit,source,mitigation; each0.0–1.0).options(object) —decay_lambda,weight_set_id,include_delta(defaulttrue),include_breakdown(defaulttrue).
Response item (ScoreEvaluateResponse) highlights:
score_id— opaque identifier for replay/lookup (score_<16-hex>).score_value(int, 0–100) andbucket(ActNow|ScheduleNext|Investigate|Watchlist).unknowns_fraction(0.0–1.0),unknowns_band, andunknowns— deterministic list of missing signal dimensions when snapshot data is available.proof_ref— deterministic proof locator (proof://score/<normalized-digest>).breakdown,guardrails,delta_if_present,conflicts,weight_manifest,ews_digest,determinization_fingerprint,computed_at.
GET /api/v1/score/{scoreId}
Returns a previously computed/persisted score by its identifier. Returns 404 with { "error": "Score not found", "score_id": "<scoreId>" } when not present. Payload is a ScoreEvaluateResponse.
GET /api/v1/score/history?cve_id=<id>&purl=<optional>&limit=<optional>
Returns historical score records for the requested CVE and optional PURL. cve_id is required; a blank value returns 400 with { "error": "cve_id query parameter is required" }. limit defaults to 50 when omitted.
Response is a ScoreListResponse<ScoreHistoryRecord>. Each record (snake_case) carries: id, tenant_id, project_id, cve_id, purl, score (decimal 0–1), band, weights_version, signal_snapshot, replay_digest, created_at.
GET /api/v1/score/weights
Lists available EWS weight manifests. Response is a ScoreListResponse<WeightManifestSummary>; each summary carries version, effective_from, profile, content_hash, description.
GET /api/v1/score/weights/{version}
Returns a specific weight manifest by version (WeightManifestDetail: schema_version, version, effective_from, profile, content_hash, description, weights). Returns 404 with { "error": "Weight manifest not found", "version": "<version>" } when not present.
GET /api/v1/score/weights/effective?as_of=<optional ISO-8601>
Returns the effective weight manifest for the supplied date (defaults to the current UTC time when as_of is omitted). Returns 404 with { "error": "No effective weight manifest found" } when none applies.
GET /api/v1/score/{scoreId}/replay
Returns the DSSE-shaped replay envelope for a previously computed score, enabling deterministic comparison by auditors. The current producer emits an empty signatures array and no Rekor inclusion proof; the legacy signed_replay_log_dsse field name is retained for compatibility. Returns 404 with { "error": "Replay log not found", "score_id": "<scoreId>" } when not present.
Response item (ScoreReplayResponse, snake_case): signed_replay_log_dsse (base64-encoded DSSE envelope), rekor_inclusion (nullable: log_index, root_hash, tree_size?, uuid?, integrated_time?), canonical_inputs[] (name, sha256, source_ref?, size_bytes?), transforms[] (name, version, params?), algebra_steps[] (signal, symbol, w, value, term), final_score, computed_at.
POST /api/v1/score/verify
Verifies a replay log and returns deterministic verification status fields. Requires score.read. Emits a Signals audit event (AuditActions.Signals.VerifyScore).
Request body (ScoreVerifyRequest, snake_case): signed_replay_log_dsse (required), original_inputs (optional: signals, weight_manifest_version), verify_rekor (default true).
Response item (ScoreVerifyResponse, snake_case): verified, replayed_score, original_score, score_matches, digest_matches, signature_valid?, rekor_proof_valid?, differences?, verified_at.
Verification details:
verifiedis computed from deterministic comparison checks (score_matches,digest_matches) plus any availablesignature_validand (whenverify_rekoris true)rekor_proof_validchecks.- On this HTTP path,
signature_validcurrently reports only structural presence of non-emptysigstrings; it is not cryptographic validation.rekor_proof_validis not established for the unsigned producer output. Use neither field as proof of authenticity until the endpoint is wired to the cryptographic replay verifier. differences[]includes field-level mismatch reasons (each{ field, expected, actual }), for examplefinal_score,ews_digest,signed_replay_log_dsse,rekor_inclusion.- Malformed replay envelopes return a deterministic
differencesentry (e.g.signed_replay_log_dsse->envelope_not_base64_utf8/envelope_not_json/payload_missing) rather than synthetic success.
GET /api/v1/score/explain/{digest}
Returns the canonical score explanation contract for a persisted replay digest. The {digest} path segment is normalized: a missing algorithm prefix is treated as sha256:, and both the algorithm and hash segments are lower-cased.
Success payload (item, ScoreExplainResponse) — note the fields are snake_case on the wire:
{
"contract_version": "score.explain.v1",
"digest": "sha256:...",
"score_id": "score_...",
"final_score": 62,
"bucket": "Investigate",
"computed_at": "2026-02-26T12:00:00Z",
"deterministic_input_hash": "sha256:...",
"replay_link": "/api/v1/score/score_x/replay",
"factors": [
{
"name": "reachability",
"weight": 0.25,
"value": 1.0,
"contribution": 0.25
}
],
"sources": [
{
"source_type": "score_history",
"source_ref": "score-history:score_x",
"source_digest": "sha256:..."
}
]
}
Deterministic error schema (/explain/{digest})
Error payload (ScoreExplainErrorResponse):
{
"code": "not_found | invalid_input | backend_unavailable",
"message": "deterministic human-readable message",
"digest": "sha256:..."
}
Status mapping:
400->invalid_input404->not_found503->backend_unavailable
Persistence
Score history (the backing store for GET /{scoreId}, GET /history, and GET /explain/{digest}) is persisted in PostgreSQL table signals.score_history (see src/Signals/StellaOps.Signals/Score/Services/PostgresScoreHistoryStore.cs). Columns: id, tenant_id, project_id, cve_id, purl, score, band, weights_version, signal_snapshot (jsonb), replay_digest, created_at. Lookups are always tenant-scoped (WHERE ... tenant_id = @tenant_id).
Client integration notes
- CLI and Web clients must treat
score.explain.v1as the current canonical contract. - The CLI
stella score explain <digest>command callsGET /api/v1/score/explain/{digest}through the authenticated Backend Router (--server/STELLAOPS_BACKEND_URL) withscore.read, normalizes the digest the same way, and reads the snake_caseitempayload.stella gate score weights list|show|diffuses the same score-read client. The publicstella score replay|bundle|verifysubcommands still target the Scanner proof-bundle API, not these Signals score-ID endpoints. - Clients must not synthesize explanation factors when
404or503is returned. digestvalues are normalized to lowercase with an explicit algorithm prefix (sha256:).
Related documentation
- Scanner Score Proofs & Reachability API — the separate Scanner proof-bundle replay surface (
/api/v1/scans/{scanId}/score/...). - Score Proofs & Reachability API Reference — combined Scanner + Attestor proof-chain reference.
- Policy Engine REST API — how trust scores feed policy-gated release decisions.
