Документация API
API для автоматизации проверок соответствия
Тот же детерминированный движок, что и на сайте, доступен по HTTP: одна проверка — одно измерение живой страницы, воспроизводимое из тех же входных данных. Без ИИ и без оценочных суждений.
- Бесплатно
- JSON · CSV · SARIF 2.1.0
- Версия в пути: /api/v1
Как получить ключ
Ключ выдаётся бесплатно. Платного тарифа нет, и доступ не продаётся.
Он привязан к контакту, оставленному в форме на странице отчёта, — так мы знаем, кому писать, если придётся что-то изменить или отозвать ключ.
- Запустите одно сканирование на главной странице.
- На странице отчёта заполните форму (e-mail или телефон).
- Напишите нам, что вам нужен ключ API, — и мы выдадим его на этот же контакт.
Ключ показывается один раз и нигде не хранится в открытом виде. Потеряли — запросите новый; старый отзывается.
Аутентификация
Ключ передаётся в заголовке `Authorization: Bearer <ключ>`. Других способов нет.
Запрос без ключа получает 401 с JSON-телом и заголовком `WWW-Authenticate`. Мы никогда не перенаправляем на страницу входа: клиент-программа последовала бы за редиректом и получила бы HTML вместо данных, не заметив ошибки.
CORS не включён намеренно. Ключ, которым можно направить браузер на любой сторонний сайт, не должен находиться в веб-странице. Используйте API только с сервера.
Точки входа
Три штуки. Версия — в пути; ломающие изменения появятся как /api/v2, а не на месте.
POST
/api/v1/scansЗапустить сканированиеТело: {"url":"https://пример.md"}. Ответ 202 с идентификатором задачи и заголовком Location. Сканирование занимает 5–45 секунд.
GET
/api/v1/scans/{id}Прочитать сканированиеПока status равен pending или running, поле result равно null — опрашивайте снова. Потокового варианта в v1 нет.
GET
/api/v1/openapiОписание OpenAPI 3.1Единственная точка входа без аутентификации: контракт можно прочитать до получения ключа.
Примеры
Полный цикл: запуск, опрос, чтение результата — и обязательная проверка на «неизмеримо».
# Запустить сканирование
curl -sS -X POST https://ongdpr.md/api/v1/scans \
-H "Authorization: Bearer $GDPR_SCAN_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"url":"https://exemplu.md"}'
# Прочитать результат (повторяйте, пока status не станет done)
curl -sS https://ongdpr.md/api/v1/scans/SCAN_ID \
-H "Authorization: Bearer $GDPR_SCAN_KEY"
# Тот же результат в формате SARIF 2.1.0
curl -sS "https://ongdpr.md/api/v1/scans/SCAN_ID?format=sarif" \
-H "Authorization: Bearer $GDPR_SCAN_KEY" -o raport.sarifconst BASE = 'https://ongdpr.md/api/v1';
const auth = { Authorization: `Bearer ${process.env.GDPR_SCAN_KEY}` };
const started = await fetch(`${BASE}/scans`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ url: 'https://exemplu.md' }),
});
if (!started.ok) throw new Error((await started.json()).error.code); // код ошибки стабилен, текст — нет
const { scanId } = await started.json();
let job;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
job = await (await fetch(`${BASE}/scans/${scanId}`, { headers: auth })).json();
} while (job.status === 'pending' || job.status === 'running');
if (job.status === 'error') throw new Error(job.error.message);
if (job.result.verdict === 'inconclusiv') {
// Это НЕ «соответствует». Страницу не удалось измерить.
console.log('nemasurat:', job.result.confidence.reason);
} else {
// реальный вердикт
console.log(job.result.verdict, job.result.score, job.result.summary.failed);
}import os, time, uuid, requests
BASE = "https://ongdpr.md/api/v1"
auth = {"Authorization": f"Bearer {os.environ['GDPR_SCAN_KEY']}"}
started = requests.post(
f"{BASE}/scans",
headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
json={"url": "https://exemplu.md"},
timeout=30,
)
started.raise_for_status() # код ошибки стабилен, текст — нет
scan_id = started.json()["scanId"]
while True:
job = requests.get(f"{BASE}/scans/{scan_id}", headers=auth, timeout=30).json()
if job["status"] not in ("pending", "running"):
break
time.sleep(3)
if job["status"] == "error":
raise RuntimeError(job["error"]["message"])
result = job["result"]
if result["verdict"] == "inconclusiv":
# score равен None. Это не соответствующий сайт, а неизмеренный.
print("nemasurat:", result["confidence"]["reason"])
else:
# реальный вердикт
print(result["verdict"], result["score"], result["summary"]["failed"])Вердикт «inconclusiv» — прочитайте это до интеграции
Если страница не отрисовалась настолько, чтобы её можно было проверить, движок не выдаёт вердикт. В этом случае verdict равен "inconclusiv", score равен null, а findings — пустой массив.
Это не ошибка и не плохой результат. Это отсутствие результата, и оно намеренно отличимо от «сайт нарушает закон». Автоматический потребитель обязан различать эти два случая: 168-байтовая пустая страница, из которой прежний движок выводил 23 нарушения, — именно тот дефект, ради которого существует этот продукт.
Объект confidence публикует измерения, на которых основано решение: длина HTML, число ссылок и число узлов DOM. В формате SARIF такой прогон помечен как executionSuccessful: false и содержит уведомление уровня error, поэтому он никогда не выглядит как «чисто».
{
"contractVersion": "2026-07-28",
"scanId": "cm7x1a2b3c4d5e6f",
"status": "done",
"result": {
"verdict": "inconclusiv",
"score": null,
"confidence": {
"sufficient": false,
"reason": "too_few_anchors",
"htmlLength": 21259,
"anchorCount": 2,
"domNodeCount": 19
},
"findings": [],
"summary": { "findings": 0, "failed": 0, "passed": 0, "warnings": 0, "notApplicable": 0 }
},
"error": null
}Форматы
Параметр ?format= работает на обеих точках входа со сканированиями.
- ?format=json
- По умолчанию. Конверт задачи с полным результатом внутри.
- ?format=csv
- RFC 4180, окончания строк CRLF, одна строка на находку. Заголовок стабилен и только дополняется: колонки не переименовываются и не меняются местами. Значения, начинающиеся с =, +, - или @, экранируются апострофом, чтобы содержимое проверяемого сайта не превратилось в формулу в вашей таблице.
- ?format=sarif
- SARIF 2.1.0, проверенный по официальной схеме OASIS, с медиатипом application/sarif+json. Находка со статусом fail становится результатом kind: "fail" с уровнем по серьёзности; warn — kind: "review"; pass и na — соответствующими видами без уровня, как того требует §3.27.10 спецификации.
Полный заголовок CSV:
scan_id,scan_status,requested_url,final_url,scanned_at_utc,verdict,score,confidence_sufficient,confidence_reason,confidence_html_length,confidence_anchor_count,confidence_dom_node_count,finding_id,category,severity,status,weight,title_ro,title_ru,detail_ro,detail_ru,remediation_ro,remediation_ru,legal_reference,legal_classification,legal_applicability,legal_evidence_strength,legal_confidence,legal_regimes,legal_citations,evidenceИдентификаторы находок
У каждой проверки есть стабильный идентификатор вида categorie.verificare — например, transport.https_forced или policy.not_crawlable. Это то, на что стоит опираться в автоматизации: тексты правятся, идентификаторы — нет.
Набор идентификаторов открыт: новые проверки приносят новые идентификаторы в любом выпуске. Клиент, который считает незнакомый идентификатор ошибкой, не совместим с v1.
Юридические метаданные каждой находки (classification, applicability, evidenceStrength, citations) говорят, насколько прямым является доказательство. Только classification: "observed" означает непосредственно наблюдённое условие.
Коды ошибок
Код — часть контракта; текст сообщения — нет. Ветвитесь по error.code, никогда по error.message.
| Код | HTTP | Значение |
|---|---|---|
unauthorized | 401 | Отсутствует заголовок Authorization, либо ключ неверен, не существует или отозван. |
key_revoked | 401 | Зарезервирован. Сейчас не используется: отозванный ключ отвечает так же, как несуществующий, чтобы их нельзя было различить. |
invalid_request | 400 | Тело запроса не является корректным JSON-объектом, либо отсутствует поле «url». |
invalid_url | 400 | Адрес отклонён фильтром сканера (недопустимый протокол, приватный адрес, некорректный URL). |
unsupported_format | 400 | Параметр ?format= не равен json, csv или sarif. |
invalid_idempotency_key | 400 | Заголовок Idempotency-Key пуст или длиннее 255 символов. |
not_found | 404 | Сканирования с таким идентификатором не существует. |
idempotency_key_conflict | 409 | Тот же ключ идемпотентности уже использовался с другим телом запроса. |
rate_limited | 429 | Превышен лимит сканирований за текущее окно. Смотрите заголовок Retry-After. |
temporarily_blocked | 429 | Временная блокировка после повторных превышений. Смотрите заголовок Retry-After. |
capacity_exceeded | 503 | Очередь сканирования заполнена. Запрос корректен — повторите позже. |
auth_unavailable | 503 | Сервис ключей недоступен. Это наша проблема, а не проблема вашего ключа. |
internal_error | 500 | Непредвиденная внутренняя ошибка. |
scan_failed | 200 | Только на уровне задачи, внутри ответа 200: сканирование выполнилось и завершилось ошибкой (например, страница не ответила). |
Лимиты
Те же механизмы, что защищают сайт. API не является обходным путём вокруг них.
| Новые сканирования | 5 за 10 минут на IP-адрес (общий бюджет с сайтом) |
|---|---|
| Одновременные сканирования | 1 на IP-адрес |
| Повторное сканирование одного хоста | Не чаще раза в 15 минут; более ранний запрос отдаётся из кэша с X-Scan-Cache: hit и не расходует бюджет |
| При превышении | 429 с Retry-After; повторные нарушения ведут к временной блокировке |
| Idempotency-Key | До 255 символов, хранится 24 часа, привязан к вашему ключу. Не переживает перезапуск сервиса. |
| Заголовки | X-RateLimit-Limit, X-RateLimit-Window, X-RateLimit-Policy на каждом ответе |
Версионирование и вывод из эксплуатации
Внутри v1 мы никогда не удаляем и не переименовываем поле, не меняем его тип и не меняем смысл существующего значения.
Новые необязательные поля и новые значения открытых перечислений (идентификаторы находок, причины confidence, юридические режимы) могут появиться в любом выпуске. Клиент обязан их терпеть.
Ломающие изменения выходят как /api/v2. Если v1 когда-нибудь будет закрыт, каждый ответ не менее 12 месяцев будет нести заголовки Deprecation и Sunset, а v2 будет доступен и задокументирован до начала этого срока.
Чего API не делает
Нет пакетной точки входа. Массовое сканирование чужих сайтов по заказу поднимает юридические вопросы, которые мы пока не разрешили: одно сканирование — это полная загрузка страницы настоящим браузером со всеми подресурсами, и цель видит трафик с нашего адреса.
Идентификаторы сканирований не привязаны к ключу: любой действующий ключ может прочитать любой идентификатор, как любой посетитель может открыть любую страницу /report/{id}.
Сканирование — не юридическая консультация. Отчёт показывает измерения и статьи, к которым они относятся; вывод делает юрист.