Перейти к содержимому

Документация API

API для автоматизации проверок соответствия

Тот же детерминированный движок, что и на сайте, доступен по HTTP: одна проверка — одно измерение живой страницы, воспроизводимое из тех же входных данных. Без ИИ и без оценочных суждений.

  • Бесплатно
  • JSON · CSV · SARIF 2.1.0
  • Версия в пути: /api/v1

Как получить ключ

Ключ выдаётся бесплатно. Платного тарифа нет, и доступ не продаётся.

Он привязан к контакту, оставленному в форме на странице отчёта, — так мы знаем, кому писать, если придётся что-то изменить или отозвать ключ.

  1. Запустите одно сканирование на главной странице.
  2. На странице отчёта заполните форму (e-mail или телефон).
  3. Напишите нам, что вам нужен ключ 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
# Запустить сканирование
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.sarif
JavaScript (Node 18+)
const 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);
}
Python
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Значение
unauthorized401Отсутствует заголовок Authorization, либо ключ неверен, не существует или отозван.
key_revoked401Зарезервирован. Сейчас не используется: отозванный ключ отвечает так же, как несуществующий, чтобы их нельзя было различить.
invalid_request400Тело запроса не является корректным JSON-объектом, либо отсутствует поле «url».
invalid_url400Адрес отклонён фильтром сканера (недопустимый протокол, приватный адрес, некорректный URL).
unsupported_format400Параметр ?format= не равен json, csv или sarif.
invalid_idempotency_key400Заголовок Idempotency-Key пуст или длиннее 255 символов.
not_found404Сканирования с таким идентификатором не существует.
idempotency_key_conflict409Тот же ключ идемпотентности уже использовался с другим телом запроса.
rate_limited429Превышен лимит сканирований за текущее окно. Смотрите заголовок Retry-After.
temporarily_blocked429Временная блокировка после повторных превышений. Смотрите заголовок Retry-After.
capacity_exceeded503Очередь сканирования заполнена. Запрос корректен — повторите позже.
auth_unavailable503Сервис ключей недоступен. Это наша проблема, а не проблема вашего ключа.
internal_error500Непредвиденная внутренняя ошибка.
scan_failed200Только на уровне задачи, внутри ответа 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}.

Сканирование — не юридическая консультация. Отчёт показывает измерения и статьи, к которым они относятся; вывод делает юрист.

Информационная рассылка о Законе 195/2024

Мы пишем, когда меняется что-то проверяемое: текст Закона 195/2024, опубликованная позиция CNPDCP или новое правило, которое начинает проверять сканер. В каждом письме сказано, что изменилось и на чём это основано.

У нас нет фиксированной периодичности, и мы её не обещаем. Мы пишем только тогда, когда есть о чём сообщить, поэтому месяцами может не быть ни одного письма.

Мы просим только адрес электронной почты. Без имени, без компании, без должности: для информационной рассылки ничего другого не нужно.

Согласие

Галочка не отмечена заранее. Это согласие отдельно от любой другой формы на сайте и не переносится между ними.

Адрес не становится подпиской в момент отправки формы. Мы присылаем письмо со ссылкой для подтверждения — она действует 24 часа и работает один раз; подписка возникает только после нажатия. До этого адрес остаётся «в ожидании», а если вы не подтвердите, он удаляется автоматически после истечения срока ссылки.

В каждом письме есть ссылка для отписки, которая срабатывает с одного нажатия — без аккаунта и без вопросов. Отписка удаляет строку из нашей базы данных, а не помечает её как неактивную.

Оператор · Правовое основание · Ваши права
Оператор:
MEGA PROMOTING S.R.L., IDNO 1019600021765, sat. Dănceni, r-nul Ialoveni, MD-6814, Republica Moldova. oleg@megapromoting.com
Правовое основание:
ваше согласие (ст. 6 Закона 195/2024), данное отдельно для этой цели. Мы храним доказательство: точный текст, который вам был показан, язык, на котором вы его прочитали, его версию и момент подтверждения.
Ваши права:
доступ, исправление, удаление, ограничение обработки, возражение, переносимость и отзыв согласия в любой момент (ст. 13–22 Закона 195/2024). Вы также можете обратиться в Национальный центр по защите персональных данных (CNPDCP), datepersonale.md.

Политика конфиденциальностиХранение данных