Skip to content

Documentation

Update quality rule

PATCH /api/v1/quality/rules/{rule}

Change thresholds or the action for a rule on an accessible site.

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
rule path yes Numeric resource identifier in your installation.
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.

Example JSON body:

{
  "params": {
    "drop_percent": 40,
    "min_previous": 100
  }
}
Field Rule
site_id Accessible site; required for a new external-API rule. PATCH cannot move a rule
name Human-readable name, up to 255 characters
kind One of the 12 vocabulary codes
params Conditions for that kind only; unknown keys are rejected
action warn by default, or mark_suspicious
is_active Boolean, defaults to true for a new rule
preset_id Optional code of one of the 24 templates, matching kind; accepts null

POST requires name and kind; PATCH accepts only fields that need updating. Choose a field from the site's declared schema. The external API cannot create or change a platform-wide rule with no site. When changing kind, send the new params instead of conditions from the old kind. Omitting params preserves conditions when the kind is unchanged. Explicit params=null resets to initial conditions for the selected kind; kinds with a required field still need that field.

warn creates a finding and allows automatic post-collection delivery. mark_suspicious creates a critical finding, flags the collection for review, and pauses automatic delivery for that collection. Manual export is not declared blocked. Neither action fixes or deletes collected records.

Response 200

This is a shortened example with placeholder numeric IDs; full fields are documented below and in the downloadable OpenAPI schema.

{
  "id": 1,
  "name": "Product count dropped",
  "kind": "row_count_drop",
  "params": {
    "drop_percent": 40,
    "min_previous": 100
  },
  "action": "warn",
  "is_active": true,
  "site": {
    "id": 1,
    "code": "parts"
  },
  "preset_id": "catalog-drop",
  "last_check": null
}
Rule field Meaning
id, site Rule identifier and site reference; site=null for a platform-wide rule
name, kind, params Name, stable kind, and saved conditions
action, is_active Action on a breach and whether future checks are enabled
preset_id Original template code or null; multiple rules may use the same template
created_at, updated_at Definition timestamps
last_check null or the latest accessible result: status, reason, reason_label, checked_at, run_id, and details

last_check.status is passed when conditions were met, breached for a violation, or skipped when no conclusion was possible. last_check=null is not a pass. Historical details.params can differ from current params after editing. Even a platform-wide rule only returns a latest check from sites the caller can access. A different team's result must not be presented as a shared rule's result.

Behavior and limits

You may update name, enabled state, kind, params, action, and preset_id. Supplied params replaces the whole parameter set; send parameters for the new kind when changing it. A supplied site_id is ignored; moving a rule is unsupported. An empty body starts nothing. Updating does not rewrite historical findings.

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.

Explicit null is accepted only for four optional bounds: min_value, max_value, min_length, and max_length. Other parameters reject null. When replacing params, omitted optional bounds are removed. Numeric and percentage comparisons are exact; rounding is for display only.

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 PATCH "$BASE/api/v1/quality/rules/1?locale=en" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "params": {
    "drop_percent": 40,
    "min_previous": 100
  }
}
JSON

Node.js · server

// Node.js server example. Save as request.mjs.
const response = await fetch(process.env.BASE + "/api/v1/quality/rules/1?locale=en", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    Accept: "application/json",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({"params":{"drop_percent":40,"min_previous":100}}),
  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

body = '{"params":{"drop_percent":40,"min_previous":100}}'.encode("utf-8")
request = urllib.request.Request(
    os.environ["BASE"] + "/api/v1/quality/rules/1?locale=en",
    method="PATCH",
    headers={"Authorization": "Bearer " + os.environ["API_KEY"],
             "Accept": "application/json", "Content-Type": "application/json"},
    data=body,
)
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/rules/1?locale=en");
curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => "PATCH",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("API_KEY"),
        "Accept: application/json",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => <<<'JSON'
{"params":{"drop_percent":40,"min_previous":100}}
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"
    "strings"
)

func main() {
    body := strings.NewReader(`{"params":{"drop_percent":40,"min_previous":100}}`)
    request, err := http.NewRequest("PATCH", os.Getenv("BASE") + "/api/v1/quality/rules/1?locale=en", body)
    if err != nil { panic(err) }
    request.Header.Set("Authorization", "Bearer " + os.Getenv("API_KEY"))
    request.Header.Set("Accept", "application/json")
    request.Header.Set("Content-Type", "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.