curl --request GET \
--url https://{tenant}.earnie.dev/v1/findings/facets \
--header 'Authorization: Bearer <token>'import requests
url = "https://{tenant}.earnie.dev/v1/findings/facets"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://{tenant}.earnie.dev/v1/findings/facets', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{tenant}.earnie.dev/v1/findings/facets",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://{tenant}.earnie.dev/v1/findings/facets"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://{tenant}.earnie.dev/v1/findings/facets")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://{tenant}.earnie.dev/v1/findings/facets")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"facets": [
{
"path": "crypto.mode",
"values": [
{
"value": "cbc",
"count": 123
}
],
"truncated": true
}
]
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}List the filterable facet values a set of findings carries, with counts
One GROUP BY over the derived facet index, not one query per value. This is the surface that makes a filter panel correct at estate scale: the review workspace filters a client-side array capped at 5,000 findings while printing the server’s true total beside it, so on a real project the filter narrows a truncated set and the count describes a different one. Counts are computed over the population the SAME narrowing parameters would return from GET /v1/findings, so a value’s count is exactly how many findings selecting it would show. The facet selection is applied disjunctively: a path’s values are counted over the population narrowed by every OTHER selected path, never by its own. Selecting one licence therefore leaves the other licences’ counts where they were, so the dropdown still offers the values that widen the selection, while every other path narrows to the selection.
curl --request GET \
--url https://{tenant}.earnie.dev/v1/findings/facets \
--header 'Authorization: Bearer <token>'import requests
url = "https://{tenant}.earnie.dev/v1/findings/facets"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://{tenant}.earnie.dev/v1/findings/facets', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{tenant}.earnie.dev/v1/findings/facets",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://{tenant}.earnie.dev/v1/findings/facets"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://{tenant}.earnie.dev/v1/findings/facets")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://{tenant}.earnie.dev/v1/findings/facets")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"facets": [
{
"path": "crypto.mode",
"values": [
{
"value": "cbc",
"count": 123
}
],
"truncated": true
}
]
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}Authorizations
An Earnie API key, sent as Authorization: Bearer sk_earnie_.... Create one under Settings > API keys. The key's scopes decide which operations it may call.
Query Parameters
Optional. Defaults to the authenticated principal's organization. When supplied it MUST match the caller's org (else 403).
One of open, assigned, resolved, superseded_by_rule, reopened, as GET /v1/findings' own state filter accepts it. An unrecognised value is rejected with 400 rather than ignored, for the same reason an unrecognised facet path is. Not declared as an inline enum deliberately. oapi-codegen names a generated enum constant by its bare value unless another enum in the same document shares it, so repeating these five values here renames the constants of unrelated operations that happen to share one. The vocabulary is findings/domain.State either way, and this endpoint checks against it directly.
Narrow to one producing scanner, as on GET /v1/findings.
Same exact-path-or-subtree match as GET /v1/findings. Do NOT pass a trailing slash.
The current selection, counted disjunctively (see above). The same <path>:<value> vocabulary and validation as GET /v1/findings' facet: values of one path are OR-ed, paths are AND-ed, and an unrecognised or non-entitled path is rejected with 400.
Free-text SUBSEQUENCE search over a finding's path, purl, basename and title: the exact haystack and matcher of GET /v1/findings/files' q, so a count here equals the number of findings that list aggregates for the same query.
Narrow to one product finding family (oss, deps, crypto, ai, provenance), as GET /v1/findings' domain. Unknown values are ignored (no filter applied), exactly as GET /v1/findings ignores them, so the counts keep describing the list. Not declared as an inline enum, for the reason state above gives.
Restrict the response to these facet paths. Repeatable. Omitted returns every path the narrowed population carries and this organization is entitled to see. An unrecognised path is rejected with 400, for the same reason the list filter rejects one.
Maximum values per facet path, ordered by count descending then value ascending. A path with more sets truncated, so a caller can tell a short list from a complete one. Out-of-range values fall back to the default.
x <= 1000Response
The facet values the narrowed findings carry
Show child attributes
Show child attributes