Quality findings
GET /api/v1/quality/findings
Read findings automatically produced for completed collections on accessible sites.
Access
Use a Bearer API key for the active workspace and runs.read permission. Writes also require a writable key and sites.write on the selected site. Only a platform operator with settings.manage, not an external key, may change platform-wide definitions. Sites and history are scoped to the current team. An agent rejects POST/PATCH/DELETE; edit definitions in the cloud.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
site_id |
query | no | Accessible site ID in the API key workspace. |
limit |
query | no | Maximum items in one response; the upper bound depends on the method. |
run_id |
query | no | Run ID associated with the record or event. |
severity |
query | no | Quality finding severity: info, warning, or critical. |
since |
query | no | Changes at or after the ISO 8601 timestamp including its time zone. |
locale |
query | no | Language of vocabulary, suggestions, and explanations. If omitted, akmelo_locale cookie is used, otherwise ru. User-defined field names are not translated. |
No body is required. locale=ru|en selects labels and explanations; without an explicit parameter the akmelo_locale cookie is used, then Russian. Kind codes and field names are not translated.
Response 200
This is a shortened example with placeholder numeric IDs; full fields are documented below and in the downloadable OpenAPI schema.
{
"data": [],
"severities": []
}
| Finding field | Meaning |
|---|---|
id, run_id, site |
Finding, collection, and accessible-site identifiers |
kind, severity, message |
Kind, importance, and localized description |
rule |
A live rule reference or null when deleted |
details |
Original kind, params, rule_name, action, field, field_name, measurements, and coverage |
explanation, next_step |
Meaning of the check and suggested next action |
created_at |
Finding timestamp |
details.checked is the number of values actually checked, affected is the violation count, percent is the displayed percentage, and threshold is the original threshold. Volume checks also expose before, after, and previous_run_id. Measurement fields depend on the kind. Historical details.origin_rule_id identifies an old definition and does not promise a live resource; rule_deleted=true marks deletion. Collected records themselves are not included.
Behavior and limits
This method cannot create findings. details stores original conditions and measurements. Inspect sample_limited, checked, affected, empty_skipped, and oversized_skipped. If a rule was deleted, rule is null and the finding remains. No findings does not show whether checks ran; inspect rules.last_check.
Rules apply to future successful complete collections. They do not alter old data or fix scenario code. Field checks use up to 5,000 records from the current collection and explicitly report a limited sample; except for completeness checks, empty values are excluded. passed, breached, and skipped are distinct, and no finding does not prove success.
Numeric examples retain the legacy v1 contract. Use X-Identifier-Format: uuid or the same method under /api/v2 for public UUIDs, including nested site, rule, and collection references. preset_id, category_id, and field remain string codes. Identifier formats.
With budget_truncated=true, the check is skipped and measurements are partial. Reaching a shared budget is not reported as a successful check.
Coverage and skipped checks
Before json_decode, JSON complexity and depth are checked: 200000 nodes and 16 MiB estimated decoded structure overall, 4 MiB per row, and 24 levels. Brackets inside strings are not nodes. Remaining PHP memory is checked before each PDO batch and decode, with an 8 MiB reserve. Additional skip reasons are sample_structure_exceeded, sample_depth_exceeded, sample_decode_error, and sample_memory_exceeded. A first row that is too complex is a budget skip, not no_records. Measurements before stopping are partial.
Only successful complete normal collections are checked. Trials, partial, failed, and cancelled collections are excluded. Continuations or retries after an interruption receive skipped / continued_run. Volume comparisons require an earlier successful complete collection from the same site and scenario that is not flagged for review; otherwise the reason is no_baseline, or baseline_too_small when its volume is too low.
Volume counters cover the full collection. Field checks use the first 5000 active records from this collection, ordered by internal ID. This is a bounded sample, not a random estimate of the whole catalog. Shared limits are 32 distinct fields, 100000 cells, and 8 MiB of raw data; an individual value over 8 KiB is not loaded into memory. sample_limited, sample_size, sample_limit, budget_truncated, budget_bytes, byte_limit, cell_limit, field_limit, and oversized_skipped describe coverage. Reaching a shared budget produces skipped / sample_budget_exceeded; partial observed_status is not a final pass or finding. Fields beyond the field limit receive field_budget_exceeded.
Empty means null, an empty string, Unicode-whitespace-only text, or an empty array. 0 and false are filled. Only field_fill_rate treats emptiness as a violation. Other kinds exclude empty values from their denominator and return no_filled_values when no filled values remain. Other skip reasons are no_records, field_missing, values_too_large, too_few_records, unsupported_kind, and check_error. A check failure does not turn an already saved successful collection into a failed one.
Shares breach strictly above the allowance: exactly 7 of 100 at 7% passes. Volume drops and growth include the threshold: 100 → 90 at a 10% threshold creates a finding. Comparisons are exact; only the displayed percentage is rounded. Freshness is measured against run.finished_at; dates with no time zone are UTC.
See Data quality for all 24 template examples and the interface workflow. For parameter definitions, see the vocabulary.
Send the request
Set BASE to your installation URL without a trailing slash and API_KEY to a secret stored on your server. Client setup and browser JavaScript.
curl
curl --fail-with-body -X GET "$BASE/api/v1/quality/findings?site_id=1&limit=100&locale=en?site_id=1&limit=100" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json"
Node.js · server
// Node.js server example. Save as request.mjs.
const response = await fetch(process.env.BASE + "/api/v1/quality/findings?site_id=1&limit=100&locale=en?site_id=1&limit=100", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
Accept: "application/json"
},
signal: AbortSignal.timeout(120000)
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
console.log(await response.json());
Python
import json
import os
import urllib.request
import urllib.error
request = urllib.request.Request(
os.environ["BASE"] + "/api/v1/quality/findings?site_id=1&limit=100&locale=en?site_id=1&limit=100",
method="GET",
headers={"Authorization": "Bearer " + os.environ["API_KEY"],
"Accept": "application/json"},
)
try:
with urllib.request.urlopen(request, timeout=120) as response:
print(json.load(response))
except urllib.error.HTTPError as error:
print(error.code, error.read().decode("utf-8", errors="replace"))
raise
PHP · external client
<?php
$handle = curl_init(getenv("BASE") . "/api/v1/quality/findings?site_id=1&limit=100&locale=en?site_id=1&limit=100");
curl_setopt_array($handle, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("API_KEY"),
"Accept: application/json",
],
]);
$content = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ($content === false) {
throw new RuntimeException(curl_error($handle));
}
curl_close($handle);
if ($status < 200 || $status >= 300) {
throw new RuntimeException("HTTP $status: $content");
}
print_r(json_decode($content, true, 512, JSON_THROW_ON_ERROR));
Go
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
request, err := http.NewRequest("GET", os.Getenv("BASE") + "/api/v1/quality/findings?site_id=1&limit=100&locale=en?site_id=1&limit=100", nil)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer " + os.Getenv("API_KEY"))
request.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 120 * time.Second}
response, err := client.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
content, err := io.ReadAll(response.Body)
if err != nil { panic(err) }
if response.StatusCode < 200 || response.StatusCode >= 300 {
panic(fmt.Sprintf("HTTP %d: %s", response.StatusCode, content))
}
fmt.Println(string(content))
}
If the request fails
401: check the key and its expiry. 403: check permissions, read/write scope, and 2FA. 404: check the ID and workspace. 422: check fields and resource state. 429: respect Retry-After. Errors and retries.