curl --request GET \
--url https://{tenant}.earnie.dev/v1/findings/components \
--header 'Authorization: Bearer <token>'import requests
url = "https://{tenant}.earnie.dev/v1/findings/components"
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/components', 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/components",
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/components"
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/components")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://{tenant}.earnie.dev/v1/findings/components")
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{
"items": [
{
"purl": "<string>",
"display_name": "<string>",
"versions": [
{
"version": "<string>",
"purl": "<string>",
"file_count": 123,
"source": "declared"
}
],
"matched_file_count": 123,
"decided_count": 123,
"total_count": 123,
"state": "open",
"vendor": "<string>",
"has_code_match": true,
"has_manifest_declared": true,
"has_crypto": true,
"scan_only": true,
"license": {
"spdx_id": "<string>",
"name": "<string>",
"url": "<string>",
"copyleft": true,
"category": "permissive",
"dual_licensed": true,
"source": "<string>"
},
"vulnerabilities": [
{
"kev": true,
"known_ransomware": true,
"fixed_versions": [
"<string>"
],
"cwe": [
"<string>"
],
"cve_id": "<string>",
"cvss_score": 123,
"cvss_vector": "<string>",
"epss_score": 123,
"epss_percentile": 123,
"severity": "critical",
"source": "<string>",
"published": "2023-11-07T05:31:56Z",
"modified": "2023-11-07T05:31:56Z"
}
],
"geoprovenance": {
"countries": [
"<string>"
],
"locations": [
{
"country": "<string>",
"percentage": 123
}
]
},
"resolution": {
"reason_slug": "<string>",
"replacement_purl": "<string>",
"concluded_license": "<string>",
"note": "<string>",
"actor": {
"kind": "user",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"decided_at": "2023-11-07T05:31:56Z"
},
"remediation": {
"purl": "<string>",
"version": "<string>",
"coverage": "assessed",
"signals": [
{
"dimension": "vulnerability",
"kind": "<string>",
"effect": "blocking",
"fact": "<string>",
"evidence": [
{
"label": "<string>",
"url": "<string>"
}
]
}
],
"confidence": 0.5,
"confidence_reasons": [
"<string>"
],
"assessed_at": "2023-11-07T05:31:56Z",
"source": {
"provider": "<string>",
"version": "<string>"
},
"priority": "fix_now",
"action": {
"kind": "upgrade",
"upgrade_pain": "none",
"summary": "<string>",
"target_version": "<string>",
"replacement_purl": "<string>"
}
}
}
],
"total": 123,
"limit": 123,
"offset": 123
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}{
"code": "<string>",
"message": "<string>"
}List findings aggregated by component (purl@version)
A crypto finding on first-party code carries no purl and is therefore not a component. Each item represents a distinct base purl, grouped across its observed versions, and a purl seen by more than one scanner is ONE item. The list reflects the project’s CURRENT posture: only present findings (absent_at IS NULL) are aggregated — findings superseded by a later scan are excluded. Enrichment (license, vulnerabilities, geoprovenance) is attached via a single batched Enricher.Enrich call — absent when no scan-time data exists for a component (not an error).
curl --request GET \
--url https://{tenant}.earnie.dev/v1/findings/components \
--header 'Authorization: Bearer <token>'import requests
url = "https://{tenant}.earnie.dev/v1/findings/components"
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/components', 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/components",
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/components"
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/components")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://{tenant}.earnie.dev/v1/findings/components")
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{
"items": [
{
"purl": "<string>",
"display_name": "<string>",
"versions": [
{
"version": "<string>",
"purl": "<string>",
"file_count": 123,
"source": "declared"
}
],
"matched_file_count": 123,
"decided_count": 123,
"total_count": 123,
"state": "open",
"vendor": "<string>",
"has_code_match": true,
"has_manifest_declared": true,
"has_crypto": true,
"scan_only": true,
"license": {
"spdx_id": "<string>",
"name": "<string>",
"url": "<string>",
"copyleft": true,
"category": "permissive",
"dual_licensed": true,
"source": "<string>"
},
"vulnerabilities": [
{
"kev": true,
"known_ransomware": true,
"fixed_versions": [
"<string>"
],
"cwe": [
"<string>"
],
"cve_id": "<string>",
"cvss_score": 123,
"cvss_vector": "<string>",
"epss_score": 123,
"epss_percentile": 123,
"severity": "critical",
"source": "<string>",
"published": "2023-11-07T05:31:56Z",
"modified": "2023-11-07T05:31:56Z"
}
],
"geoprovenance": {
"countries": [
"<string>"
],
"locations": [
{
"country": "<string>",
"percentage": 123
}
]
},
"resolution": {
"reason_slug": "<string>",
"replacement_purl": "<string>",
"concluded_license": "<string>",
"note": "<string>",
"actor": {
"kind": "user",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"decided_at": "2023-11-07T05:31:56Z"
},
"remediation": {
"purl": "<string>",
"version": "<string>",
"coverage": "assessed",
"signals": [
{
"dimension": "vulnerability",
"kind": "<string>",
"effect": "blocking",
"fact": "<string>",
"evidence": [
{
"label": "<string>",
"url": "<string>"
}
]
}
],
"confidence": 0.5,
"confidence_reasons": [
"<string>"
],
"assessed_at": "2023-11-07T05:31:56Z",
"source": {
"provider": "<string>",
"version": "<string>"
},
"priority": "fix_now",
"action": {
"kind": "upgrade",
"upgrade_pain": "none",
"summary": "<string>",
"target_version": "<string>",
"replacement_purl": "<string>"
}
}
}
],
"total": 123,
"limit": 123,
"offset": 123
}{
"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
Project to aggregate findings for.
Filter to a specific domain slug.
Free-text search across component name and purl.
Keep the components with at least one finding matching the selection. A kept component's counts (decided_count, total_count, state) still cover ALL of its findings: the selection decides which components appear, never how much of one is reviewed. 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.
Filter by component state (open, resolved, partial).
open, resolved, partial Aggregate the components as this scan saw them. For any other scan the parameter changes nothing: the list stays the project-wide one.
Sort order. Default is open-first (open components before resolved).
open_first, name_asc Maximum number of components to return. 0 returns the complete component set in a single response (no pagination). Values above 100 fall back to the default of 20, preserving prior behavior.
0 <= x <= 100