Sari la conținut

Documentație API

API pentru automatizarea verificărilor de conformitate

Același motor determinist care rulează pe site, expus prin HTTP: o verificare înseamnă o măsurătoare a paginii vii, reproductibilă din aceleași date de intrare. Fără AI și fără aprecieri.

  • Gratuit
  • JSON · CSV · SARIF 2.1.0
  • Versiune în cale: /api/v1

Cum obții cheia

Cheia se dă gratuit. Nu există plan cu plată și nu vindem acces.

Este legată de contactul lăsat în formularul de pe pagina raportului — așa știm cui să scriem dacă e nevoie să schimbăm ceva sau să revocăm cheia.

  1. Rulează o scanare pe pagina principală.
  2. Pe pagina raportului, completează formularul (email sau telefon).
  3. Scrie-ne că vrei o cheie de API — o emitem pe același contact.

Cheia se arată o singură dată și nu este păstrată nicăieri în clar. Dacă o pierzi, ceri alta; cea veche se revocă.

Autentificare

Cheia se trimite în antetul `Authorization: Bearer <cheie>`. Nu există altă cale.

O cerere fără cheie primește 401 cu corp JSON și antetul `WWW-Authenticate`. Nu redirecționăm niciodată către o pagină de autentificare: un client automat ar urma redirecționarea și ar primi HTML în loc de date, fără să observe eroarea.

CORS nu este activat, deliberat. O cheie care poate îndrepta un browser către orice site terț nu are ce căuta într-o pagină web. Folosește API-ul doar de pe server.

Rute

Trei. Versiunea stă în cale; o schimbare care rupe compatibilitatea apare ca /api/v2, nu pe loc.

  • POST/api/v1/scansPornește o scanare

    Corp: {"url":"https://exemplu.md"}. Răspuns 202 cu identificatorul jobului și antetul Location. O scanare durează 5–45 de secunde.

  • GET/api/v1/scans/{id}Citește o scanare

    Cât timp status este pending sau running, câmpul result este null — reinterogează. În v1 nu există variantă cu flux continuu.

  • GET/api/v1/openapiDescrierea OpenAPI 3.1

    Singura rută fără autentificare: contractul se poate citi înainte de a cere o cheie.

Exemple

Ciclul complet: pornire, interogare, citirea rezultatului — plus verificarea obligatorie de „nemăsurabil".

curl
# Pornește o scanare
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"}'

# Citește rezultatul (repetă până când status devine done)
curl -sS https://ongdpr.md/api/v1/scans/SCAN_ID \
  -H "Authorization: Bearer $GDPR_SCAN_KEY"

# Același rezultat, în format 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); // codul erorii este stabil, textul nu
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') {
  // NU înseamnă „conform". Pagina nu a putut fi măsurată.
  console.log('nemasurat:', job.result.confidence.reason);
} else {
  // verdictul real
  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()  # codul erorii este stabil, textul nu
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 este None. Nu e un site conform, e un site nemăsurat.
    print("nemasurat:", result["confidence"]["reason"])
else:
    # verdictul real
    print(result["verdict"], result["score"], result["summary"]["failed"])

Verdictul „inconclusiv" — citește asta înainte de a integra

Când o pagină nu se randează suficient cât să poată fi auditată, motorul nu emite verdict. În acest caz verdict este "inconclusiv", score este null, iar findings este un tablou gol.

Nu e o eroare și nu e un rezultat prost. Este absența unui rezultat, și este intenționat distinctă de „site-ul încalcă legea". Un consumator automat trebuie să poată deosebi cele două: o pagină goală de 168 de octeți din care motorul deducea cândva 23 de neconformități este exact defectul pentru care există acest produs.

Obiectul confidence publică măsurătorile pe care s-a luat decizia: lungimea HTML, numărul de legături și numărul de noduri DOM. În SARIF, o astfel de rulare este marcată executionSuccessful: false și poartă o notificare de nivel error, deci nu arată niciodată a „curat".

Răspuns
{
  "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
}

Formate

Parametrul ?format= funcționează pe ambele rute de scanare.

?format=json
Implicit. Plicul jobului, cu rezultatul complet în interior.
?format=csv
RFC 4180, terminatori CRLF, un rând per constatare. Antetul este stabil și doar se extinde: coloanele nu se redenumesc și nu se rearanjează. Valorile care încep cu =, +, - sau @ primesc un apostrof, ca nu cumva conținutul site-ului scanat să devină formulă în foaia ta de calcul.
?format=sarif
SARIF 2.1.0, validat față de schema oficială OASIS, servit cu tipul media application/sarif+json. O constatare cu status fail devine un rezultat kind: "fail" cu nivel dat de severitate; warn devine kind: "review"; pass și na primesc felurile corespunzătoare fără nivel, așa cum cere §3.27.10 din specificație.

Antetul CSV complet:

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

Identificatorii constatărilor

Fiecare verificare are un identificator stabil de forma categorie.verificare — de exemplu transport.https_forced sau policy.not_crawlable. Pe el se automatizează: textele se editează, identificatorii nu.

Mulțimea identificatorilor este deschisă: verificări noi aduc identificatori noi în orice versiune. Un client care tratează un identificator necunoscut ca eroare nu este compatibil cu v1.

Metadatele juridice ale fiecărei constatări (classification, applicability, evidenceStrength, citations) spun cât de directă este dovada. Doar classification: "observed" înseamnă o condiție observată nemijlocit.

Coduri de eroare

Codul face parte din contract; textul mesajului nu. Ramifică pe error.code, niciodată pe error.message.

CodHTTPCe înseamnă
unauthorized401Lipsește antetul Authorization, sau cheia este greșită, inexistentă ori revocată.
key_revoked401Rezervat. Nu este emis astăzi: o cheie revocată răspunde identic cu una inexistentă, ca să nu se poată afla care este care.
invalid_request400Corpul cererii nu este un obiect JSON valid, sau lipsește câmpul „url".
invalid_url400Adresa a fost respinsă de filtrul scanerului (protocol nepermis, adresă privată, URL invalid).
unsupported_format400Parametrul ?format= nu este json, csv sau sarif.
invalid_idempotency_key400Antetul Idempotency-Key este gol sau depășește 255 de caractere.
not_found404Nu există nicio scanare cu acest identificator.
idempotency_key_conflict409Aceeași cheie de idempotență a fost folosită deja cu un alt corp de cerere.
rate_limited429Ai depășit limita de scanări pentru fereastra curentă. Vezi antetul Retry-After.
temporarily_blocked429Blocare temporară după depășiri repetate. Vezi antetul Retry-After.
capacity_exceeded503Coada de scanare este plină. Cererea este corectă — reia mai târziu.
auth_unavailable503Serviciul de chei nu este disponibil. Este o problemă la noi, nu la cheia ta.
internal_error500Eroare internă neprevăzută.
scan_failed200Doar la nivel de job, în interiorul unui răspuns 200: scanarea a rulat și a eșuat (de exemplu, pagina nu a răspuns).

Limite

Aceleași mecanisme care apără site-ul. API-ul nu este o cale de ocolire a lor.

Scanări noi5 la fiecare 10 minute per adresă IP (buget comun cu site-ul)
Scanări simultane1 per adresă IP
Rescanarea aceleiași gazdeCel mult o dată la 15 minute; o cerere mai devreme primește raportul din cache, cu X-Scan-Cache: hit, și nu consumă buget
La depășire429 cu Retry-After; depășirile repetate duc la blocare temporară
Idempotency-KeyMaximum 255 de caractere, reținută 24 de ore, legată de cheia ta. Nu supraviețuiește unei reporniri a serviciului.
AnteteX-RateLimit-Limit, X-RateLimit-Window, X-RateLimit-Policy pe fiecare răspuns

Versionare și depreciere

În interiorul v1 nu ștergem și nu redenumim niciun câmp, nu îi schimbăm tipul și nu schimbăm înțelesul unei valori existente.

Câmpuri opționale noi și valori noi în mulțimile deschise (identificatori de constatări, motive de confidence, regimuri juridice) pot apărea în orice versiune. Clientul trebuie să le tolereze.

Schimbările care rup compatibilitatea apar ca /api/v2. Dacă v1 va fi vreodată retras, fiecare răspuns va purta antetele Deprecation și Sunset cu cel puțin 12 luni înainte, iar v2 va fi disponibil și documentat înainte ca termenul să înceapă.

Ce nu face API-ul

Nu există rută de lot. Scanarea în masă a site-urilor terților la comandă ridică întrebări juridice pe care nu le-am rezolvat încă: o scanare înseamnă încărcarea completă a paginii cu un browser real, cu toate subresursele, iar ținta vede trafic venind de la adresa noastră.

Identificatorii de scanare nu sunt legați de cheie: orice cheie validă poate citi orice identificator, la fel cum orice vizitator poate deschide orice pagină /report/{id}.

O scanare nu este consultanță juridică. Raportul arată măsurători și articolele la care se referă; concluzia o trage juristul.

Buletin informativ despre Legea 195/2024

Îți scriem când se schimbă ceva verificabil: textul Legii 195/2024, o poziție publicată de CNPDCP sau o regulă nouă pe care scanerul începe să o verifice. Fiecare mesaj spune ce s-a schimbat și de unde știm.

Nu avem o periodicitate fixă și nu îți promitem una. Trimitem doar când există o schimbare de raportat, așa că pot trece luni fără niciun mesaj.

Cerem doar adresa de email. Fără nume, fără companie, fără funcție: pentru a trimite un buletin informativ nu ne trebuie nimic altceva.

Consimțământ

Căsuța nu este bifată dinainte. Acest consimțământ este separat de orice alt formular de pe site și nu se transferă între ele.

Adresa nu devine abonament la trimiterea formularului. Îți trimitem un email cu un link de confirmare, valabil 24 de ore și folosibil o singură dată; abonamentul există doar după ce dai clic pe el. Până atunci adresa rămâne „în așteptare", iar dacă nu confirmi se șterge automat după expirarea linkului.

Fiecare mesaj conține un link de dezabonare care funcționează dintr-un singur clic, fără cont și fără întrebări. Dezabonarea șterge rândul din baza noastră de date, nu îl marchează ca inactiv.

Operator · Temei · Drepturile tale
Operator:
MEGA PROMOTING S.R.L., IDNO 1019600021765, sat. Dănceni, r-nul Ialoveni, MD-6814, Republica Moldova. oleg@megapromoting.com
Temei:
consimțământul tău (art. 6 din Legea 195/2024), dat separat pentru acest scop. Păstrăm dovada: textul exact care ți-a fost afișat, limba în care l-ai citit, versiunea lui și momentul în care ai confirmat.
Drepturile tale:
acces, rectificare, ștergere, restricționarea prelucrării, opoziție, portabilitate și retragerea consimțământului în orice moment (art. 13–22 din Legea 195/2024). Te poți adresa și Centrului Național pentru Protecția Datelor cu Caracter Personal (CNPDCP), datepersonale.md.

Politica de confidențialitateRetenția datelor