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|history

The Scanner replay surface uses a different contract (proof-bundle / Merkle root hash) and is authorized with scanner:read (GET) / scanner:write (POST). See src/Scanner/StellaOps.Scanner.WebService/Endpoints/ScoreReplayEndpoints.cs and docs/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

signals.score.read / signals.score.evaluate are ASP.NET policy names; score.read / score.evaluate are canonical claim values in StellaOpsScopes and 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 wrapped item/items payloads use the explicit snake_case names declared by the contract DTOs’ [JsonPropertyName] attributes (for example score_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:

Request body (ScoreEvaluateRequest, snake_case):

Response item (ScoreEvaluateResponse) highlights:

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:

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:

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