Metadata-Version: 2.5
Name: acteo
Version: 0.4.1
Summary: SDK Python oficial pentru API-ul public Acteo: verificare RCA, ITP, rovinieta si copie conforma ARR dupa numar de inmatriculare sau serie de sasiu (VIN), plus decodare VIN, numere de inmatriculare disponibile, poze de masini, monitorizarea flotei si webhook-uri.
Project-URL: Homepage, https://acteo.ro/api-acces
Project-URL: Documentation, https://github.com/acteoro/sdk_python#readme
Project-URL: Repository, https://github.com/acteoro/sdk_python
Project-URL: Changelog, https://github.com/acteoro/sdk_python/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/acteoro/sdk_python/issues
Author-email: "Acteo.ro" <contact@acteo.ro>
License-Expression: MIT
License-File: LICENSE
Keywords: acteo,api,asigurare auto,car insurance check,copie conforma arr,decodare vin,dgpci,fleet management,flota auto,imagini masini,itp,numar inmatriculare disponibil,numere disponibile,poze masini,rca,road tax,romania,rovinieta,sdk,vehicle inspection,verificare arr,verificare itp,verificare rca,verificare rovinieta,verificrca,vin decoder
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Romanian
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: anyio<5,>=3.7
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.6
Description-Content-Type: text/markdown

# acteo: API verificare RCA, ITP, rovinietă și ARR pentru Python

[![PyPI](https://img.shields.io/pypi/v/acteo.svg)](https://pypi.org/project/acteo/)
[![Python](https://img.shields.io/pypi/pyversions/acteo.svg)](https://pypi.org/project/acteo/)
[![licență MIT](https://img.shields.io/pypi/l/acteo.svg)](./LICENSE)

SDK-ul oficial Python pentru [API-ul Acteo](https://acteo.ro/api-acces). Cu el verifici din cod dacă o mașină are RCA valabil, ITP la zi, rovinietă plătită sau copie conformă ARR, după numărul de înmatriculare sau seria de șasiu (VIN). Datele vin din sursele oficiale: baza AIDA pentru asigurarea RCA, RAR pentru inspecția tehnică periodică, CNAIR pentru rovinietă și ARR pentru transportul rutier.

Pe lângă verificările la cerere, SDK-ul decodează serii de șasiu, verifică la DGPCI dacă un număr de înmatriculare e liber, aduce poze de studio ale mașinilor, gestionează vehiculele monitorizate din cont și citește webhook-urile trimise când un document se apropie de expirare. Are client sincron (Django, Flask, scripturi, Celery) și asincron (FastAPI, asyncio), cu tipuri complete. Dacă ai o firmă de leasing, rent-a-car sau curierat, un service auto ori pur și simplu o flotă, afli din timp ce RCA, ITP sau rovinietă expiră, înainte să vină amenda.

> Ai nevoie de o cheie API. O generezi din [contul tău Acteo](https://app.acteo.ro/api/chei), după ce alegi un plan de pe [pagina API](https://acteo.ro/api-acces).

```python
from acteo import Acteo

with Acteo() as acteo:  # cheia din ACTEO_API_KEY
    rca = acteo.auto.rca.verifica("B123ABC")
    print(rca.status.value, rca.expires_at)  # valid 2027-03-01 00:00:00+00:00
```

Apelurile sunt grupate pe domeniu și pe document, deci citești din cod ce verifici: `acteo.auto.rca`, `acteo.auto.itp`, `acteo.auto.rovinieta`, `acteo.auto.arr`, plus `acteo.auto.flota` pentru vehiculele din cont, `acteo.auto.vin`, `acteo.auto.numere` și `acteo.auto.imagini` pentru celelalte produse și `acteo.webhooks` pentru notificări.

## Cuprins

- [Ce face SDK-ul](#ce-face-sdk-ul)
- [Instalare](#instalare)
- [Pornire rapidă](#pornire-rapidă)
- [Client async](#client-async)
- [Ce identificator cere fiecare verificare](#ce-identificator-cere-fiecare-verificare)
- [Cum decurge o verificare](#cum-decurge-o-verificare)
- [Referință API](#referință-api)
- [Webhook-uri](#webhook-uri)
- [Erori](#erori)
- [Reîncercări, backoff și timeout](#reîncercări-backoff-și-timeout)
- [Cotă și limite](#cotă-și-limite)
- [Exemple](#exemple)
- [Vii de la verificrca?](#vii-de-la-verificrca)
- [Întrebări frecvente](#întrebări-frecvente)

## Ce face SDK-ul

| Funcție | Apel | Ce primești |
|---|---|---|
| Verificare RCA după număr sau VIN | `acteo.auto.rca.verifica()` | Starea poliței RCA și data expirării, din baza AIDA |
| Verificare ITP după VIN | `acteo.auto.itp.verifica()` | Data până la care e valabilă inspecția tehnică, din RAR |
| Verificare rovinietă | `acteo.auto.rovinieta.verifica()` | Dacă rovinieta e plătită și până când, din CNAIR |
| Verificare copie conformă ARR | `acteo.auto.arr.verifica()` | Valabilitatea copiei conforme pentru transportatori |
| Gestionare flotă | `acteo.auto.flota` | Adaugi, listezi și ștergi vehiculele monitorizate automat |
| Decodare VIN | `acteo.auto.vin.decodeaza()` | Marca, modelul, anul de fabricație și structura seriei de șasiu |
| Număr de înmatriculare disponibil | `acteo.auto.numere.verifica()` | Dacă numărul e liber la DGPCI și poate fi rezervat |
| Poza unei mașini | `acteo.auto.imagini.obtine()` | Link spre o poză de studio (WebP transparent) după marcă, model și an |
| Webhook-uri | `acteo.webhooks` | Rezultatul verificării la URL-ul tău, alerte `document.expira` la 30, 14 și 3 zile, plus evenimentele celorlalte servicii din cont |
| Validare locală | | Numărul de înmatriculare, VIN-ul și numărul talonului sunt verificate înainte de cerere, deci o greșeală de tastare nu consumă din cotă |
| Excepții tipate | | O clasă de excepție pentru fiecare situație, cu mesaj în română și `hint` de rezolvare |
| Reîncercări sigure | | Backoff exponențial cu jitter, `Retry-After` respectat, niciun POST taxat de două ori |
| Sync și async | | `Acteo` pentru cod sincron, `AsyncActeo` pentru asyncio și FastAPI, cu aceleași metode |
| Tipuri complete | | Modele Pydantic imuabile, `py.typed`, verificat cu mypy în mod strict |

## Instalare

```bash
pip install acteo
# sau
uv add acteo
poetry add acteo
```

SDK-ul cere Python 3.10 sau mai nou și folosește [httpx](https://www.python-httpx.org/) pentru cereri și [Pydantic](https://docs.pydantic.dev/) pentru modele.

Cheia API o poți da direct în constructor (`Acteo(api_key="...")`) sau prin variabila de mediu `ACTEO_API_KEY`:

```bash
export ACTEO_API_KEY=cheia_ta_api
```

Nu pune cheia în cod care ajunge într-un repo public sau într-o aplicație instalată pe telefonul clienților. Cheia dă acces la cota contului tău și la vehiculele din el, deci o folosești doar pe server.

## Pornire rapidă

### Verificare RCA după număr de înmatriculare

```python
from acteo import Acteo, StatusDocument

with Acteo() as acteo:  # citește ACTEO_API_KEY
    rca = acteo.auto.rca.verifica("B 123 ABC")

if rca.status is StatusDocument.EXPIRED:
    print("Mașina nu are RCA valabil.")
elif rca.status is StatusDocument.WARNING and rca.expires_at:
    print(f"RCA-ul expiră curând, pe {rca.expires_at:%d.%m.%Y}")
```

Un text de 17 caractere fără literele I, O și Q e tratat ca VIN, orice altceva ca număr de înmatriculare. Pentru RCA îți recomandăm să trimiți ambele. Baza AIDA găsește polița mai sigur după seria de șasiu, iar uneori o poliță emisă pe VIN nu apare la căutarea după număr:

```python
rca = acteo.auto.rca.verifica(numar="B123ABC", vin="WVWZZZ1JZXW000001")
```

### Verificare ITP după serie de șasiu

```python
itp = acteo.auto.itp.verifica("WVWZZZ1JZXW000001")
print(f"ITP valabil până la {itp.expires_at:%d.%m.%Y}" if itp.expires_at else itp.status.value)
```

### Verificare rovinietă

Rovinieta cere trei date: numărul de înmatriculare, seria de șasiu (VIN) și numărul certificatului de înmatriculare (`serie_talon`), tipărit jos pe talon. Sursa oficială le validează pe toate trei împreună, așa că fără numărul talonului SDK-ul oprește cererea înainte să plece și nu consumă din cotă.

```python
rovinieta = acteo.auto.rovinieta.verifica(
    numar="B123ABC",
    vin="WVWZZZ1JZXW000001",
    serie_talon="I00639603F",
)
print(rovinieta.status.value, rovinieta.expires_at)
```

Dacă sursa respinge datele (de exemplu, numărul talonului nu corespunde mașinii), primești `EroareVerificare` cu `cod_sursa` `"serie_talon_invalida"`. O reîncercare cu aceleași date nu ajută; corectează întâi câmpul indicat:

```python
from acteo import EroareVerificare

try:
    acteo.auto.rovinieta.verifica(numar="B123ABC", vin=vin, serie_talon=talon)
except EroareVerificare as e:
    if e.cod_sursa == "serie_talon_invalida":
        cere_utilizatorului_numarul_talonului()
    else:
        raise
```

Vezi și [unde găsești numărul certificatului de înmatriculare](#unde-găsesc-numărul-certificatului-de-înmatriculare).

### Verificare copie conformă ARR

```python
arr = acteo.auto.arr.verifica("CJ45XYZ")
```

### Decodare VIN

```python
d = acteo.auto.vin.decodeaza("WVWZZZ1KZBW000000")
print(d.marca, d.model, d.an_fabricatie)  # Volkswagen Golf 2011
print(d.structura.wmi, d.structura.cifra_control)  # WVW neaplicabila
```

Decodarea consumă din cotă doar când găsește marca și modelul (`taxat` e `True`). Fără ele primești totuși structura VIN-ului (WMI, VDS, VIS, țara, cifra de control, anii posibili), calculată din cele 17 caractere.

### Verifică dacă un număr de înmatriculare e liber

```python
r = acteo.auto.numere.verifica("BV 07 ABC")
if r.disponibil:
    print(f"{r.numar} e liber")
```

Răspunsul vine direct de la DGPCI, în aceeași cerere, și poate dura până la două minute. SDK-ul dă acestei cereri un timeout de cel puțin 150 de secunde, oricât de mic e `timeout_ms` din client. Consumă din cotă doar când DGPCI dă un verdict.

### Poza unei mașini

```python
p = acteo.auto.imagini.obtine(marca="Volkswagen", model="Golf", an=2015, unghi="fata")
if p.status == "gata":
    afiseaza(p.url)  # link valabil până la p.expira_la

# sau așteaptă cât timp poza e încă în lucru:
gata = acteo.auto.imagini.asteapta(marca="Volkswagen", model="Golf", an=2015)
```

Cu `status == "gata"` primești poza mașinii cerute și cererea se taxează. Altfel primești poza generică (mașina sub husă), netaxată: `in-lucru` înseamnă că poza se generează acum, `necunoscut` că marca sau modelul nu au fost identificate, iar `indisponibil` că generarea a eșuat.

## Client async

`AsyncActeo` are aceleași metode, aceleași argumente și aceleași excepții, doar că le apelezi cu `await`. E potrivit pentru FastAPI, Starlette, aiohttp sau orice cod asyncio, și îți permite să rulezi mai multe verificări în paralel:

```python
import asyncio

from acteo import ActeoError, AsyncActeo


async def main() -> None:
    numere = ["B123ABC", "CJ45XYZ", "B12XYZ"]
    async with AsyncActeo() as acteo:
        rezultate = await asyncio.gather(
            *(acteo.auto.rca.verifica(n) for n in numere),
            return_exceptions=True,
        )
    for numar, r in zip(numere, rezultate):
        if isinstance(r, ActeoError):
            print(f"{numar}: {r.mesaj}")
        elif not isinstance(r, BaseException):
            print(f"{numar}: {r.status.value}")


asyncio.run(main())
```

Pauzele dintre interogări folosesc `anyio.sleep`, deci nu blochează event loop-ul.

## Ce identificator cere fiecare verificare

Fiecare sursă oficială caută după alt identificator, așa că fiecare document își cere exact datele lui. SDK-ul aplică aceleași reguli ca API-ul și te oprește înainte de cerere dacă lipsește ceva.

| Apel | Ce primește | Sursa |
|---|---|---|
| `auto.rca.verifica(x)` | număr de înmatriculare **sau** VIN, ori `numar=..., vin=...` (recomandat) | AIDA / BAAR |
| `auto.itp.verifica(vin)` | VIN (17 caractere) | RAR |
| `auto.rovinieta.verifica(numar=..., vin=..., serie_talon=...)` | toate trei, **obligatorii** | CNAIR |
| `auto.arr.verifica(numar)` | număr de înmatriculare | ARR |

Formatele de număr acceptate: București (`B123ABC`, `B12ABC`), județe (`CJ45XYZ`, `IS08BCD`), corp diplomatic (`CD123A`) și numere temporare (`TC1234AB`). Spațiile și cratimele sunt eliminate automat, deci `b-123 abc` devine `B123ABC`. VIN-ul trebuie să aibă exact 17 caractere, fără literele I, O și Q (standardul ISO 3779).

`serie_talon` este numărul certificatului de înmatriculare, de forma `I00639603F`. SDK-ul păstrează doar literele și cifrele, cu majuscule, și cere între 6 și 20 de caractere, cu cel puțin o cifră. Doar sursa oficială poate confirma că numărul aparține mașinii.

## Cum decurge o verificare

1. **Cerere** (`porneste`). Fiecare cerere interoghează sursa oficială: verificarea intră în coadă și primești un obiect `VerificareInCurs` (`status == "processing"`).
2. **Interogarea sursei.** Sursa oficială e consultată în fundal. Dacă nu răspunde, verificarea se încheie cu eroare.
3. **Rezultatul** ajunge la tine pe una din trei căi:
   - `verifica()` face tot drumul și întoarce direct rezultatul final;
   - `rezultat()` sau `asteapta()` fac polling, fără să consume din cotă;
   - webhook: dacă trimiți `callback_url` la `porneste()`, API-ul face POST cu rezultatul la adresa ta.

O verificare consumă din cota planului doar când sursa oficială a răspuns, inclusiv atunci când refuză datele trimise. Dacă sursa nu răspunde, nu consumi nimic. Interogările de rezultat sunt gratuite.

### Statusuri

Rezultatul final are `status` de tip `StatusDocument`:

| `status` | Înseamnă | `expires_at` |
|---|---|---|
| `StatusDocument.VALID` (`"valid"`) | Document valabil | data expirării |
| `StatusDocument.WARNING` (`"warning"`) | Expiră curând: RCA în 14 zile, ITP în 30, rovinietă în 7, ARR în 14 | data expirării |
| `StatusDocument.EXPIRED` (`"expired"`) | Expirat sau inexistent, confirmat de sursă | data expirării sau `None` |
| `StatusDocument.NOT_FOUND` (`"not_found"`) | Sursa nu a găsit vehiculul | `None` |

O verificare încă în curs e un obiect `VerificareInCurs`, nu un rezultat. Le deosebești cu `isinstance()`.

Când verificarea se încheie cu eroare, SDK-ul aruncă `EroareVerificare` (`cod` e mereu `VERIFICARE_ESUATA`). Dacă sursa oficială doar nu a răspuns, `cod_sursa` e `None` și poți relua cererea. Dacă sursa a refuzat datele trimise, `cod_sursa` spune ce e greșit și reîncercarea nu ajută până nu corectezi câmpul:

| `cod_sursa` | Câmpul de corectat |
|---|---|
| `serie_talon_invalida` | `serie_talon`, numărul certificatului de înmatriculare |
| `vin_invalid` | `vin` |
| `numar_invalid` | numărul de înmatriculare |

## Referință API

### `Acteo(...)` și `AsyncActeo(...)`

Constructorul primește doar argumente numite, toate opționale:

| Argument | Tip | Implicit | Descriere |
|---|---|---|---|
| `api_key` | `str \| None` | variabila `ACTEO_API_KEY`, apoi `VERIFICRCA_API_KEY` | Cheia API |
| `base_url` | `str \| None` | `https://api.acteo.ro` | Adresa API-ului |
| `timeout_ms` | `float` | `30000` | Timeout pe fiecare cerere HTTP, aplicat clientului construit de SDK |
| `retry` | `SetariRetry \| dict \| bool` | `SetariRetry(incercari=3, intarziere_initiala_ms=500, intarziere_maxima_ms=10000)` | Politica de reîncercare; `False` o oprește |
| `http_client` | `httpx.Client` / `httpx.AsyncClient` | construit de SDK | Client propriu (proxy, certificate, teste) |
| `user_agent` | `str \| None` | | Adăugat la `User-Agent`, util în loguri |

Constructorul nu face nicio cerere pe rețea. Aruncă `EroareConfigurare` dacă lipsește cheia sau dacă o opțiune e invalidă.

Folosește clientul cu `with` (sau `async with`), ori apelează `close()` / `await aclose()` la final, ca să închizi conexiunile. Un `http_client` dat de tine își păstrează propriul timeout și nu e închis de SDK. Redirecturile nu sunt urmate: un răspuns 3xx devine `EroareHttp`.

### `auto.<document>.verifica(identificator, *, interval_ms=3000, timeout_ms=300000)`

Disponibil pe `auto.rca`, `auto.itp`, `auto.rovinieta` și `auto.arr`. Pornește verificarea și așteaptă rezultatul final. E calea cea mai simplă pentru scripturi și joburi cron. `interval_ms` are minimum 2000, iar `timeout_ms` e implicit 5 minute. La depășire aruncă `EroareTimeoutVerificare`.

Rezultatul are tipul documentului: `auto.rca.verifica()` întoarce un `RezultatRca` (cu `tip` mereu `TipVerificare.RCA`), `auto.itp.verifica()` un `RezultatItp` și așa mai departe. Toate sunt subclase ale `VerificareFinala`, deci codul scris pe `VerificareFinala` merge neschimbat.

### `auto.<document>.porneste(identificator, *, callback_url=None)`

Pornește verificarea fără să aștepte și **consumă din cotă**. Întoarce un obiect `VerificareInCurs` pe care îl dai mai departe la `rezultat()` sau `asteapta()`.

```python
from acteo import VerificareInCurs

r = acteo.auto.itp.porneste(
    "WVWZZZ1JZXW000001",
    callback_url="https://example.com/webhooks/acteo/9f3k2",  # opțional
)

if isinstance(r, VerificareInCurs):
    print("În coadă, request_id:", r.request_id)
    final = acteo.auto.itp.asteapta(r)
```

### `auto.<document>.rezultat(referinta)`

Interoghează o singură dată. Nu consumă din cotă. Primește obiectul `VerificareInCurs` întors de `porneste()` sau direct identificatorul documentului:

```python
acteo.auto.rca.rezultat("B123ABC")
acteo.auto.rca.rezultat(numar="B123ABC", vin="WVWZZZ1JZXW000001")  # caută după VIN
acteo.auto.itp.rezultat("WVWZZZ1JZXW000001")
acteo.auto.rovinieta.rezultat(numar="B123ABC", vin="WVWZZZ1JZXW000001")
```

La RCA cu VIN, API-ul caută doar după VIN, deci și interogarea merge după VIN. La rovinietă interogarea folosește numărul și VIN-ul; `serie_talon` e acceptat, ca să poți trimite aceleași argumente ca la `verifica()`, dar nu e necesar.

### `auto.<document>.asteapta(referinta, *, interval_ms=3000, timeout_ms=300000)`

Face polling până la rezultatul final, cu aceleași argumente ca `rezultat()` și aceleași opțiuni ca `verifica()`. Nu consumă din cotă. Reîncercările se opresc la termen, așa că doar o cerere HTTP deja pornită îl poate depăși, cu cel mult timeout-ul ei.

### `auto.flota.lista()`

Toate vehiculele din cont, cu datele de expirare `rca_expira_la`, `itp_expira_la`, `rovinieta_expira_la` și `arr_expira_la` (ca `datetime` sau `None`). Câmpul `activ` e `True` cât timp vehiculul e verificat automat și primește notificări de expirare. `serie_talon` e numărul certificatului de înmatriculare salvat pentru vehicul (sau `None`), iar `serie_talon_respinsa` devine `True` când sursa oficială l-a respins: rovinieta nu se mai verifică până când îl corectezi. Vehiculele din cont sunt verificate automat și periodic, deci lista îți dă o imagine la zi a flotei fără să consume din cota de verificări.

```python
for v in acteo.auto.flota.lista():
    rca = f"{v.rca_expira_la:%d.%m.%Y}" if v.rca_expira_la else "-"
    print(v.numar_inmatriculare, "RCA:", rca)
```

### `auto.flota.adauga(*, numar_inmatriculare=None, serie_sasiu=None, serie_talon=None, arr_monitorizat=None)`

Adaugă un vehicul în monitorizare. Trimite cel puțin un identificator (`numar_inmatriculare`, `serie_sasiu`); opționale sunt `serie_talon` și `arr_monitorizat`. ITP-ul se monitorizează doar cu VIN, iar rovinieta cere numărul, VIN-ul și numărul talonului.

```python
v = acteo.auto.flota.adauga(
    numar_inmatriculare="B123ABC",
    serie_sasiu="WVWZZZ1JZXW000001",
    serie_talon="I00639603F",
)
```

Poți primi `EroareInterzis` cu `cod` `LIMIT_ATINS` când ai atins numărul de mașini din plan, sau `EroareConflict` când vehiculul există deja.

### `auto.flota.sterge(id)`

Scoate vehiculul din cont. Aruncă `EroareNegasit` dacă ID-ul nu există în contul cheii.

Operațiile pe flotă nu consumă din cota de verificări, dar cer un plan activ.

### `auto.vin.decodeaza(vin)`

Decodează o serie de șasiu. Întoarce `DecodareVin` cu `marca`, `model`, `an_fabricatie`, `sursa_an` (`"pozitia-10"`, `"generatie"` sau `"anunturi"`), `modele_posibile` și `structura` (`wmi`, `vds`, `vis`, `regiune`, `tara`, `cifra_control`, `cifra_control_asteptata`, `ani_posibili`). `gasit` spune dacă marca e cunoscută, iar `taxat` dacă cererea s-a consumat din cotă. VIN-ul e validat local înainte de cerere. Cere un abonament de decodare VIN.

### `auto.numere.verifica(numar)`

Verifică la DGPCI un singur număr complet: indicativul județului, cifrele și trei litere (`BV07ABC`, `B123ABC`). Numerele incomplete sau cu `?` și `*` sunt respinse local. Întoarce `NumarDisponibil` cu `numar` (normalizat), `disponibil` și `mesaj` (textul DGPCI, sau `None`). Dacă DGPCI nu acceptă numărul primești `EroareValidare` cu `cod` `"NUMAR_RESPINS"`. Când verificarea nu poate rula primești `EroareServer` cu `cod` `OCUPAT`, `INDISPONIBIL`, `EROARE_DGPCI` sau `TIMEOUT`. Niciuna nu se taxează, iar `OCUPAT` se reîncearcă singur după `Retry-After`. Cere un abonament de numere disponibile.

### `auto.imagini.obtine(*, marca, model, an, unghi=None, umbra=None)`

Răspunde imediat cu poza mașinii sau cu poza generică. `unghi` poate fi `"trei-sferturi"` (implicit), `"fata"`, `"profil"`, `"spate"`, `"trei-sferturi-dreapta"` sau `"profil-dreapta"`, iar `umbra` e implicit `True`. Întoarce `ImagineMasina` cu `gata`, `status`, `taxat`, `url`, `expira_la` (`datetime`), `latime`, `inaltime`, `marca` și `model` (forma canonică) și `generatie` (`eticheta`, `an_de`, `an_pana`, `caroserie`, sau `None`). La `in-lucru`, `retry_after_ms` spune când să ceri din nou. Linkul expiră după 24 de ore. Fiecare cerere a unei poze gata se taxează, deci păstrează linkul până la `expira_la`. Cere un abonament de imagini mașini.

### `auto.imagini.asteapta(*, marca, model, an, unghi=None, umbra=None, timeout_ms=120000)`

Ca `obtine()`, dar cât timp poza e `in-lucru` o cere din nou, cu pauzele din `Retry-After` (cel puțin 10 secunde). Întoarce primul răspuns care nu mai e `in-lucru`. După `timeout_ms` (implicit 2 minute) aruncă `EroareTimeoutVerificare`. Cererile intermediare nu se taxează, dar intră în plafonul zilnic de poze generice.

### Utilitare de validare

```python
from acteo import (
    este_numar_inmatriculare_valid,
    este_serie_talon_valida,
    este_vin_valid,
    normalizeaza,
    normalizeaza_serie_talon,
)

este_numar_inmatriculare_valid("CJ 45 XYZ")  # True
este_vin_valid("WVWZZZ1JZXW00000I")  # False: litera I nu e permisă în VIN
normalizeaza(" b-123 abc ")  # "B123ABC"
este_serie_talon_valida("I00639603F")  # True
normalizeaza_serie_talon(" i 00639603-f ")  # "I00639603F"
```

Le poți folosi în formularele tale (de exemplu într-un validator Django sau Pydantic), ca să verifici datele înainte să ajungă la server.

### Constante

| Constantă | Valoare |
|---|---|
| `TIPURI_VERIFICARE` | `TipVerificare.RCA`, `ITP`, `ROVINIETA`, `ARR`, util pentru dropdown-uri și validare |
| `UNGHIURI_IMAGINE` | unghiurile acceptate de `auto.imagini` |
| `HEADER_CHEIE` | `"x-acteo-key"`, headerul cu cheia de webhook |
| `HEADER_EVENIMENT` | `"x-acteo-event"`, headerul cu numele evenimentului |
| `URL_IMPLICIT` | `"https://api.acteo.ro"` |
| `VERSIUNE_SDK` | versiunea pachetului, trimisă în `User-Agent` |

### Documentația tipurilor

Rezultatele sunt modele Pydantic imuabile, cu câmpuri în snake_case (`expires_at`, `checked_at`, `numar_inmatriculare`) și datele ca `datetime` cu fus orar. Pe fir, API-ul folosește camelCase; `model_dump_json(by_alias=True)` îți dă exact forma din API. Pachetul are `py.typed` și docstring-uri pe fiecare metodă, deci IDE-ul (VS Code cu Pylance, PyCharm) și mypy văd toate tipurile, inclusiv documentul fiecărui rezultat.

## Webhook-uri

API-ul trimite două feluri de webhook. SDK-ul le validează strict și îți dă modele tipate, cu datele ca `datetime`. Corpul poate fi `bytes`, `str` sau un `dict` deja parsat. `acteo.webhooks` nu face cereri pe rețea, deci îl folosești la fel din cod sincron și async.

### Rezultatul unei verificări (`callback_url`)

Când trimiți `callback_url` la `porneste()`, primești un POST cu rezultatul imediat ce e gata. Dacă sursa nu răspunde, primești `status` `StatusWebhook.ERROR`.

```python
from fastapi import FastAPI, Request

from acteo import Acteo, StatusWebhook

app = FastAPI()
acteo = Acteo()


@app.post("/webhooks/acteo/rezultat/9f3k2x")
async def rezultat(request: Request) -> dict[str, bool]:
    r = acteo.webhooks.parseaza_rezultat(await request.body())
    if r.status is not StatusWebhook.ERROR and r.result:
        salveaza(r.request_id, r.result.expires_at)
    return {"ok": True}
```

Pune un segment greu de ghicit în URL, păstrează-l pe HTTPS și verifică dacă `request_id`-ul primit corespunde celui întors de `porneste()` înainte să te bazezi pe conținut.

### Notificări din cont (`document.expira` și celelalte evenimente)

Pe planurile care includ webhook-uri, configurezi un URL în dashboard și primești câte un eveniment când un document al unui vehicul monitorizat intră într-un prag de expirare. Pentru RCA, ITP și rovinietă pragurile sunt 30, 14 și 3 zile, iar pentru ARR 60, 50 și 45 de zile. Fiecare cerere vine cu cheia contului în headerul `X-Acteo-Key` și cu numele evenimentului în `X-Acteo-Event`. SDK-ul o compară în timp constant, ca să nu dezvăluie nimic prin durata comparației.

`acteo.webhooks.verifica_notificare(corp, headere, cheie)` primește headerele în forma în care ți le dă framework-ul: `request.headers` din FastAPI, Starlette, Flask sau Django, `request.META` din Django ori un simplu `dict`.

**FastAPI:**

```python
import os

from fastapi import FastAPI, HTTPException, Request

from acteo import Acteo, EroareWebhook, WebhookDocumentExpira

app = FastAPI()
acteo = Acteo()
CHEIE_WEBHOOK = os.environ["ACTEO_WEBHOOK_KEY"]


@app.post("/webhooks/acteo")
async def notificare(request: Request) -> dict[str, bool]:
    try:
        ev = acteo.webhooks.verifica_notificare(await request.body(), request.headers, CHEIE_WEBHOOK)
    except EroareWebhook as e:
        raise HTTPException(status_code=401 if e.cod in ("CHEIE_INVALIDA", "CHEIE_NECONFIGURATA") else 400) from e
    if isinstance(ev, WebhookDocumentExpira):
        print(f"{ev.vehicul.numar_inmatriculare}: {ev.document.value} expiră în {ev.zile_ramase} zile")
    return {"ok": True}
```

**Django** (exclude view-ul din protecția CSRF):

```python
import os

from django.http import HttpRequest, HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from acteo import Acteo, EroareWebhook, WebhookDocumentExpira

acteo = Acteo()


@csrf_exempt
@require_POST
def notificare(request: HttpRequest) -> HttpResponse:
    try:
        ev = acteo.webhooks.verifica_notificare(request.body, request.headers, os.environ["ACTEO_WEBHOOK_KEY"])
    except EroareWebhook as e:
        return JsonResponse({"error": e.cod}, status=400)
    if isinstance(ev, WebhookDocumentExpira):
        trimite_alerta.delay(ev.vehicul.id, ev.document.value, ev.zile_ramase)
    return JsonResponse({"ok": True})
```

**Flask:**

```python
@app.post("/webhooks/acteo")
def notificare():
    try:
        ev = acteo.webhooks.verifica_notificare(request.get_data(), request.headers, os.environ["ACTEO_WEBHOOK_KEY"])
    except EroareWebhook as e:
        return jsonify(error=e.cod), 400
    return jsonify(ok=True)
```

Codurile `EroareWebhook`:

| `cod` | Când |
|---|---|
| `CHEIE_NECONFIGURATA` | Ai trimis o cheie goală (de obicei lipsește variabila de mediu) |
| `CHEIE_INVALIDA` | Headerul `X-Acteo-Key` lipsește sau nu se potrivește |
| `JSON_INVALID` | Corpul nu e JSON |
| `PAYLOAD_INVALID` | Evenimentul nu are forma așteptată |

Folosește `ev.id` pentru deduplicare. Un eveniment poate ajunge de mai multe ori dacă serverul tău nu răspunde cu 2xx. Butonul de test din dashboard trimite un `WebhookTest` (`event` `webhook.test`).

Cu add-on-ul Webhook, la același URL vin și evenimentele celorlalte servicii din cont. `verifica_notificare()` le validează pe toate; le distingi după `event` sau cu `isinstance`:

| `event` | Model | Când vine | Câmpuri principale |
|---|---|---|---|
| `document.expira` | `WebhookDocumentExpira` | Un document al unui vehicul intră într-un prag de expirare | `vehicul`, `document`, `expira_la`, `zile_ramase`, `prag` |
| `dosar.eveniment` | `WebhookDosarEveniment` | Noutate într-un dosar monitorizat | `dosar`, `eveniment.tip`, `eveniment.descriere` |
| `dosar.termen` | `WebhookDosarTermen` | Se apropie o ședință, cu 3 zile și cu o zi înainte | `dosar`, `sedinta`, `zile_ramase` |
| `firma.schimbare` | `WebhookFirmaSchimbare` | ANAF a schimbat starea unei firme monitorizate | `firma`, `schimbari` |
| `firma.dosar` | `WebhookFirmaDosar` | Dosar nou în instanță pe numele unei firme | `firma`, `dosar.insolventa` |
| `marca.schimbare` | `WebhookMarcaSchimbare` | Schimbare la o marcă urmărită (OSIM sau EUIPO) | `marca`, `schimbare`, `valoare_veche`, `valoare_noua` |
| `marca.expira` | `WebhookMarcaExpira` | O marcă intră într-un prag de expirare | `marca`, `expira_la`, `prag`, `reinnoire_pana_la` |
| `asigurare.schimbare` | `WebhookAsigurareSchimbare` | S-a schimbat statutul de asigurat la CNAS | `persoana`, `statut`, `are_card` |
| `termen.expira` | `WebhookTermenExpira` | Un termen din cont intră în pragul de 30 sau de 3 zile | `termen`, `zile_ramase`, `prag` |
| `webhook.test` | `WebhookTest` | Butonul de test din dashboard | `mesaj` |

Datele cu oră (`trimis_la`, `sedinta.data`, `verificat_la`) devin `datetime` cu fus orar. Datele calendaristice `yyyy-mm-dd` (`expira_la` la `marca.expira` și `termen.expira`) rămân text. Tipul `WebhookNotificare` e uniunea tuturor modelelor.

### Verificarea în joburi de coadă

`VerificareInCurs` se salvează cu `model_dump_json()` și se reface cu `VerificareInCurs.model_validate_json()`. Așa pornești verificarea într-un proces și citești rezultatul în altul, de exemplu cu Celery:

```python
from acteo import Acteo, VerificareInCurs


@app.task
def verifica_rca(numar: str) -> None:
    with Acteo() as acteo:
        pornita = acteo.auto.rca.porneste(numar)
    if isinstance(pornita, VerificareInCurs):
        citeste_rezultat.apply_async(args=[pornita.model_dump_json()], countdown=10)


@app.task
def citeste_rezultat(date: str) -> None:
    with Acteo() as acteo:
        rezultat = acteo.auto.rca.asteapta(VerificareInCurs.model_validate_json(date))
    salveaza(rezultat)
```

Excepțiile SDK-ului trec prin `pickle` cu toate atributele, deci apar corect și în rezultatele joburilor eșuate.

## Erori

Toate excepțiile moștenesc `ActeoError`, care moștenește `Exception`. Fiecare are `mesaj` (în română, la fel ca `str(e)`), plus atributele `status`, `cod`, `hint` și `raspuns` când vin de la API. Eroarea originală (de rețea, de decodare) e în `__cause__`.

| Clasă | Când apare | Câmpuri utile |
|---|---|---|
| `EroareConfigurare` | Cheie lipsă sau opțiuni invalide | `hint` |
| `EroareValidare` | Date greșite, local sau HTTP 400; 422 când DGPCI refuză numărul (`NUMAR_RESPINS`) | `camp`, `acceptate`, `cod` |
| `EroareAutentificare` | 401: cheie invalidă sau dezactivată | `hint` |
| `EroarePlanInactiv` | 402: fără abonament activ | `hint` |
| `EroareInterzis` | 403: `LIMIT_ATINS`, `ARR_NECESITA_PLAN` | `cod`, `limita` |
| `EroareNegasit` | 404: vehicul inexistent în cont | |
| `EroareConflict` | 409: `VEHICUL_EXISTENT` | |
| `EroareLimita` | 429: cotă, plafon de flotă sau polling prea des | `cod`, `retry_after_ms`, `utilizare_zi`, `utilizare_luna` |
| `EroareServer` | 5xx; la numere disponibile `OCUPAT`, `INDISPONIBIL`, `EROARE_DGPCI`, `TIMEOUT` | `status`, `cod`, `retry_after_ms` |
| `EroareHttp` | Alt status HTTP neașteptat, inclusiv redirecturi | `status` |
| `EroareRetea` | Fără răspuns sau timeout | `timeout`, `neajunsa` |
| `EroareRaspunsInvalid` | Răspuns cu formă neașteptată | `raspuns` |
| `EroareVerificare` | Sursa nu a răspuns sau a refuzat datele trimise; `cod` e mereu `VERIFICARE_ESUATA` | `cod_sursa`: `serie_talon_invalida`, `vin_invalid`, `numar_invalid` sau `None` |
| `EroareTimeoutVerificare` | `asteapta()` sau `imagini.asteapta()` a depășit `timeout_ms` | |
| `EroareWebhook` | Cheie greșită sau payload invalid | `cod` |

```python
from acteo import ActeoError, EroareLimita, EroareValidare

try:
    acteo.auto.rca.porneste(numar)
except EroareValidare as e:
    return arata_utilizatorului(e.mesaj)
except EroareLimita as e:
    if e.cod == "COTA_ATINSA":
        return amana_pana_maine()
    raise
except ActeoError as e:
    logger.error(e.mesaj, extra={"cod": e.cod, "hint": e.hint})
    raise
```

## Reîncercări, backoff și timeout

Erorile trecătoare sunt reîncercate automat, cu backoff exponențial (x2) și jitter, plafonat la `intarziere_maxima_ms`. Când serverul trimite `Retry-After`, pauza nu coboară sub valoarea cerută.

SDK-ul nu trimite niciodată de două ori o verificare taxată.

| Situație | GET / DELETE | POST |
|---|---|---|
| Rețea căzută, timeout la citirea răspunsului | se reîncearcă | nu (cererea poate să fi ajuns) |
| Redirect (3xx) | nu, `EroareHttp` | nu |
| DNS, conexiune refuzată, eroare TLS, timeout la conectare | se reîncearcă | se reîncearcă (sigur nu a ajuns) |
| 502, 503, 504 fără `cod` | se reîncearcă | se reîncearcă (proxy, nu aplicația) |
| 503 `OCUPAT` la numere disponibile | | se reîncearcă după `Retry-After`, dacă acesta nu depășește `intarziere_maxima_ms` (netaxat) |
| 502, 503, 504 cu alt `cod` | | nu (netaxat, dar repetarea nu ajută imediat) |
| 500 | se reîncearcă | nu |
| 429 la polling | se reîncearcă după `Retry-After`, dacă acesta nu depășește `intarziere_maxima_ms` | |
| 429 cotă sau plafon flotă | nu | nu |
| 4xx | nu | nu |

```python
from acteo import Acteo, SetariRetry

acteo = Acteo(
    timeout_ms=15_000,
    retry=SetariRetry(incercari=5, intarziere_initiala_ms=250, intarziere_maxima_ms=8_000),
)
# sau: retry={"incercari": 5}, ori retry=False ca să le oprești
```

## Cotă și limite

Fiecare verificare consumă o unitate din cota zilnică și lunară a planului, dar numai când sursa oficială a răspuns, inclusiv atunci când refuză datele trimise. Decodarea VIN, numerele disponibile și pozele au fiecare abonamentul și cota lor: decodarea se taxează doar cu marcă și model găsite, numărul doar când DGPCI dă un verdict, iar poza doar când e gata. Decodările fără rezultat și pozele generice au un plafon zilnic separat. Dacă sursa nu răspunde, nu consumi nimic. Verificările încă în curs ocupă cota până la răspuns, ca să nu treci de limită cu cereri trimise în paralel.

Rezultatele (inclusiv decodările, numerele și pozele) au câmpul `limite`, completat din headerele `X-RateLimit-*`:

```python
r = acteo.auto.rca.porneste("B123ABC")
if r.limite:
    print(r.limite.zi.ramase)  # 287
    print(r.limite.luna.limita)  # 10000
```

Cota zilnică se resetează la miezul nopții, ora României. Limitele fiecărui plan le găsești pe [pagina API](https://acteo.ro/api-acces).

## Exemple

Folderul [`examples/`](./examples) conține scripturi gata de rulat:

- [`verificare_rca.py`](./examples/verificare_rca.py): verificare RCA după număr (și VIN, opțional), cu tratarea erorilor;
- [`flota.py`](./examples/flota.py): adaugă vehicule și afișează ce RCA, ITP, rovinietă sau ARR expiră în 30 de zile;
- [`async_verificari.py`](./examples/async_verificari.py): mai multe verificări în paralel cu clientul async;
- [`webhook_fastapi.py`](./examples/webhook_fastapi.py), [`webhook_django.py`](./examples/webhook_django.py), [`webhook_flask.py`](./examples/webhook_flask.py): primirea webhook-urilor.

### Verificare în masă

```python
from acteo import Acteo, ActeoError

with Acteo() as acteo:
    for numar in ["B123ABC", "CJ45XYZ", "IS08BCD"]:
        try:
            r = acteo.auto.rca.verifica(numar)
            print(numar, r.status.value)
        except ActeoError as e:
            print(numar, e.mesaj)
```

Pentru liste mari, adaugă vehiculele în cont cu `auto.flota.adauga()`. Sunt monitorizate automat și primești notificările de expirare, fără să consumi din cota de verificări.

## Vii de la verificrca?

Pachetul `verificrca` s-a mutat la `acteo`. Instalezi pachetul nou și schimbi apelurile:

| Înainte | Acum |
|---|---|
| `pip install verificrca` | `pip install acteo` |
| `import verificrca` / `from verificrca import ...` | `import acteo` / `from acteo import ...` |
| `VerificRca()` / `AsyncVerificRca()` | `Acteo()` / `AsyncActeo()` |
| `VERIFICRCA_API_KEY` | `ACTEO_API_KEY` (cea veche e citită în continuare, dacă lipsește cea nouă) |
| `verificari.verifica_si_asteapta(tip="rca", query=q)` | `auto.rca.verifica(q)` |
| `verificari.verifica_si_asteapta(tip="rca", query=numar, serie_sasiu=vin)` | `auto.rca.verifica(numar=numar, vin=vin)` |
| `verificari.verifica_si_asteapta(tip="itp", query=vin)` | `auto.itp.verifica(vin)` |
| `verificari.verifica_si_asteapta(tip="rovinieta", query=numar, serie_sasiu=vin, serie_talon=talon)` | `auto.rovinieta.verifica(numar=numar, vin=vin, serie_talon=talon)` |
| `verificari.verifica_si_asteapta(tip="arr", query=numar)` | `auto.arr.verifica(numar)` |
| `verificari.porneste(...)` / `rezultat(...)` / `asteapta(...)` | `auto.<document>.porneste` / `rezultat` / `asteapta` |
| `vehicule.lista()` / `adauga()` / `sterge()` | `auto.flota.lista()` / `adauga()` / `sterge()` |
| `parseaza_webhook_rezultat()` / `verifica_webhook_notificare()` | `webhooks.parseaza_rezultat()` / `webhooks.verifica_notificare()` |
| `VerificRcaError` | `ActeoError` |

Pachetul `acteo` include și modulul `verificrca`, deci `import verificrca` și codul scris pe clientul vechi merg în continuare după `pip install acteo`, cu aceleași obiecte. Excepțiile, modelele și utilitarele de validare au rămas aceleași.

## Întrebări frecvente

### Cum verific RCA-ul unei mașini din Python?

Instalezi pachetul cu `pip install acteo`, creezi un client cu cheia API și apelezi `acteo.auto.rca.verifica("B123ABC")`. Primești statusul poliței și data expirării, din baza de date AIDA. Pentru o verificare manuală, fără cod, folosește pagina [verificare RCA](https://acteo.ro/verificare-rca).

### Pot verifica ITP-ul după numărul de înmatriculare?

Nu. RAR caută inspecția tehnică periodică doar după seria de șasiu (VIN), așa că SDK-ul respinge cererea înainte să plece, fără să consume din cotă. VIN-ul îl găsești în talon, la rubrica E.

### Unde găsesc numărul certificatului de înmatriculare?

E tipărit jos pe talon (certificatul de înmatriculare), la „Numărul certificatului”, și arată ca `I00639603F`. Nu îl confunda cu seria de șasiu (VIN), care e la rubrica E și are 17 caractere. Îl trimiți în `serie_talon` la verificarea rovinietei sau la `auto.flota.adauga()`. Spațiile și cratimele sunt eliminate automat. Dacă sursa oficială îl respinge, primești `EroareVerificare` cu `cod_sursa` `serie_talon_invalida`, iar la vehiculele din flotă `serie_talon_respinsa` devine `True`.

### Cum verific rovinieta unei mașini din Python?

Apelezi `acteo.auto.rovinieta.verifica(numar="B123ABC", vin="WVWZZZ1JZXW000001", serie_talon="I00639603F")`. Primești statusul și data până la care e plătită rovinieta, din CNAIR. Sursa oficială validează împreună cele trei date, așa că fără numărul talonului cererea e oprită înainte să plece. Pentru o verificare manuală, folosește pagina [verificare rovinietă](https://acteo.ro/verificare-rovinieta).

### De unde vin datele?

Din sursele oficiale: AIDA (baza de date a asigurărilor RCA administrată de BAAR), Registrul Auto Român pentru ITP, CNAIR pentru rovinietă și Autoritatea Rutieră Română pentru copia conformă. Nu estimăm nimic: fiecare rezultat e exact ce a răspuns sursa.

### De ce o poliță RCA nouă apare ca expirată?

Asigurătorul are nevoie de timp ca să înregistreze polița nouă în AIDA, de obicei între 1 și 7 zile. În acest interval verificarea poate arăta polița veche. Detalii despre [amenzile pentru RCA expirat](https://acteo.ro/amenzi/rca).

### Merge cu Django, Flask sau FastAPI?

Da. SDK-ul nu depinde de niciun framework. În Django și Flask folosești `Acteo`, în FastAPI și alte aplicații asyncio `AsyncActeo`. Webhook-urile acceptă direct `request.headers` din oricare dintre ele. Cheia API rămâne pe server.

### Pot folosi SDK-ul în Celery sau alte joburi de coadă?

Da. `VerificareInCurs` se serializează în JSON, așa că pornești verificarea într-un job și citești rezultatul în altul (exemplul din [Verificarea în joburi de coadă](#verificarea-în-joburi-de-coadă)). Excepțiile trec prin `pickle` fără să-și piardă atributele.

### Cum verific dacă un număr de înmatriculare e liber?

Apelezi `acteo.auto.numere.verifica("BV07ABC")`. SDK-ul trimite numărul la DGPCI și îți întoarce `disponibil` `True` dacă e liber și poate fi rezervat. Trimite numărul complet, cu județ, cifre și trei litere; căutările cu `?` sau `*` nu sunt acceptate.

### Ce aflu dintr-o serie de șasiu (VIN)?

Cu `acteo.auto.vin.decodeaza(vin)` afli marca, modelul și anul de fabricație, plus structura VIN-ului: producătorul (WMI), țara, cifra de control și anii posibili. Câmpul `sursa_an` spune de unde vine anul, iar `modele_posibile` apare când datele nu indică un singur model.

### Există SDK pentru Node.js sau PHP?

Da, cu aceleași apeluri: [`@acteo/api`](https://github.com/acteoro/sdk_javascript) pentru Node.js și TypeScript și [`acteo/api`](https://github.com/acteoro/sdk_php) pentru PHP. [Documentația API](https://acteo.ro/api-acces) are și exemple pentru cURL.

## Linkuri utile

- [Documentația API Acteo](https://acteo.ro/api-acces)
- [Verificare RCA online](https://acteo.ro/verificare-rca)
- [Verificare ITP online](https://acteo.ro/verificare-itp)
- [Verificare rovinietă online](https://acteo.ro/verificare-rovinieta)
- [Verificare copie conformă ARR](https://acteo.ro/verificare-arr)
- [Monitorizare flote auto](https://acteo.ro/flote)
- [Changelog](./CHANGELOG.md)

## Dezvoltare

```bash
uv sync
uv run pytest                                   # teste unitare, fără rețea
uv run mypy                                     # tipuri, mod strict
uv run ruff check && uv run ruff format --check # stil
uv run pytest e2e                               # teste pe API-ul real
```

Testele e2e citesc configurarea din `.env.e2e`, fișier ignorat de git. Pornești de la șablon:

```bash
cp .env.e2e.example .env.e2e
```

Fără cheie rulează doar testul de autentificare. Cu `ACTEO_API_KEY` se adaugă listarea vehiculelor, care e gratuită. Verificările reale (`ACTEO_E2E_VERIFICARI=1`) consumă din cotă, iar testul de flotă (`ACTEO_E2E_FLOTA=1`) adaugă și șterge un vehicul. Amândouă pornesc doar dacă le activezi explicit.

## Licență

[MIT](./LICENSE), © Acteo.ro
