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.
- Rulează o scanare pe pagina principală.
- Pe pagina raportului, completează formularul (email sau telefon).
- 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 scanareCorp: {"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 scanareCâ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.1Singura 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".
# 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.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); // 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);
}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".
{
"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,evidenceIdentificatorii 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.
| Cod | HTTP | Ce înseamnă |
|---|---|---|
unauthorized | 401 | Lipsește antetul Authorization, sau cheia este greșită, inexistentă ori revocată. |
key_revoked | 401 | Rezervat. Nu este emis astăzi: o cheie revocată răspunde identic cu una inexistentă, ca să nu se poată afla care este care. |
invalid_request | 400 | Corpul cererii nu este un obiect JSON valid, sau lipsește câmpul „url". |
invalid_url | 400 | Adresa a fost respinsă de filtrul scanerului (protocol nepermis, adresă privată, URL invalid). |
unsupported_format | 400 | Parametrul ?format= nu este json, csv sau sarif. |
invalid_idempotency_key | 400 | Antetul Idempotency-Key este gol sau depășește 255 de caractere. |
not_found | 404 | Nu există nicio scanare cu acest identificator. |
idempotency_key_conflict | 409 | Aceeași cheie de idempotență a fost folosită deja cu un alt corp de cerere. |
rate_limited | 429 | Ai depășit limita de scanări pentru fereastra curentă. Vezi antetul Retry-After. |
temporarily_blocked | 429 | Blocare temporară după depășiri repetate. Vezi antetul Retry-After. |
capacity_exceeded | 503 | Coada de scanare este plină. Cererea este corectă — reia mai târziu. |
auth_unavailable | 503 | Serviciul de chei nu este disponibil. Este o problemă la noi, nu la cheia ta. |
internal_error | 500 | Eroare internă neprevăzută. |
scan_failed | 200 | Doar 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 noi | 5 la fiecare 10 minute per adresă IP (buget comun cu site-ul) |
|---|---|
| Scanări simultane | 1 per adresă IP |
| Rescanarea aceleiași gazde | Cel 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ășire | 429 cu Retry-After; depășirile repetate duc la blocare temporară |
| Idempotency-Key | Maximum 255 de caractere, reținută 24 de ore, legată de cheia ta. Nu supraviețuiește unei reporniri a serviciului. |
| Antete | X-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.