# FirmeAPI.ro — API Listă Firme: ghid de implementare pentru agenți AI

> Acest document e scris pentru agenți AI de programare (Claude Code, Cursor, Copilot, Codex, ChatGPT etc.).
> Conține tot ce trebuie pentru o integrare completă, fără întrebări suplimentare către utilizator.
> Versiunea pentru oameni: https://www.firmeapi.ro/info-api/lista-firme

Caută firme după denumire și filtrează după județ, localitate, cod CAEN, stare, TVA, telefon sau data înregistrării. Plătești doar firmele primite: 0,70 credite per rezultat, iar o căutare fără rezultate e gratuită.

## 0. Ce trebuie să livrezi

Integrează în aplicația utilizatorului API-ul de căutare și listare firme FirmeAPI.ro, cap-coadă:

1. **Configurare** — cheia API citită din variabila de mediu `FIRMEAPI_KEY`; adaug-o în `.env.example` (sau echivalent) cu valoare goală. Nu scrie cheia în cod și nu o trimite niciodată în browser.
2. **Client API** — un modul/serviciu în backend cu câte o metodă pentru fiecare endpoint (`cautaFirme`), cu tipurile răspunsurilor, tratarea erorilor (secțiunea 4) și reîncercări doar unde e permis.
3. **Cache** — rezultatele păstrate pe cheia cererii (de exemplu 24 de ore pentru datele unei firme). Fiecare cerere reușită consumă credite.
4. **Logica de domeniu** — regulile din secțiunea 5, într-un singur loc, nu împrăștiate prin interfață.
5. **Interfață** (dacă aplicația are UI) — afișarea datelor și a mesajelor de eroare; câmpurile `null` apar ca „nedeclarat” sau „—”, nu ca 0.
6. **Teste** — pe modul sandbox (secțiunea 3), fără credite, care acoperă cazurile din secțiunea 8.

Respectă convențiile proiectului existent (limbaj, framework, structură de foldere, client HTTP deja folosit).

## 1. Autentificare și cereri

- Bază URL: `https://www.firmeapi.ro/api/v1`
- Header de autentificare: `X-API-KEY: <cheie>` (alternativ `Authorization: Bearer <cheie>`).
- Cheia se creează din contul FirmeAPI, pagina „Chei API”.
- Trimite `Accept: application/json`. Răspunsurile sunt JSON UTF-8; datele calendaristice au formatul `YYYY-MM-DD`.
- Timeout recomandat: 30 s.

## 2. Endpoint-urile

### 2.1 Căutare firme — `GET /firme`

Cost: **0,70 / rezultat** per cerere reușită (2xx). Erorile nu se taxează.

Până la 20 de firme per pagină, ordonate după relevanță și apoi după data înregistrării (cele mai noi primele).

| Parametru | Unde | Tip | Obligatoriu | Descriere |
|---|---|---|---|---|
| `q` | query | string | nu | Caută în denumire (toleră greșeli de scriere). Formele juridice (SRL, SA) sunt ignorate. Ex.: `popescu tech`. |
| `judet` | query | string | nu | Codul auto al județului, 2 litere. Ex.: `CJ, B, TM`. |
| `localitate` | query | string | nu | Localitatea sediului social. Ex.: `Cluj-Napoca`. |
| `caen` | query | string | nu | Cod CAEN: 4 cifre pentru o clasă exactă sau 1–3 cifre pentru toate clasele care încep așa (62 = toate 62xx). Merge cu sau fără zeroul din față (0111 și 111 dau același rezultat). Caută codul așa cum e la ANAF, din oricare revizie. Ex.: `6201`. |
| `stare` | query | string | nu | Starea de înregistrare ANAF. Ex.: `INREGISTRAT, RADIERE`. |
| `tva` | query | 0\|1 | nu | 1 = doar plătitori de TVA, 0 = doar neplătitori. |
| `telefon` | query | 0\|1 | nu | 1 = doar firme cu telefon declarat, 0 = doar fără. |
| `data` | query | AAAA-LL-ZZ | nu | Firmele înregistrate exact în ziua dată. |
| `data_start · data_end` | query | AAAA-LL-ZZ | nu | Interval pentru data înregistrării (oricare poate lipsi). Ex.: `2026-01-01`. |
| `page` | query | integer | nu | Pagina, de la 1. Fiecare pagină are 20 de firme. |

Exemplu: `GET https://www.firmeapi.ro/api/v1/firme?judet=CJ&caen=6201&tva=1`

Răspuns 200 (date fictive, aceeași formă ca răspunsul real):

```json
{
    "success": true,
    "data": {
        "items": [
            {
                "cui": 12345678,
                "denumire": "POPESCU TECH S.R.L.",
                "data_inregistrare": "2020-03-15",
                "stare": "INREGISTRAT din data 15.03.2020",
                "cod_caen": "6201",
                "nr_reg_com": "J12/1234/2020",
                "telefon": "0264123456",
                "fax": "0212345678",
                "cod_postal": "400683",
                "adresa": "JUD. CLUJ, MUN. CLUJ-NAPOCA, ALEEA NECTARULUI, NR.9",
                "act_autorizare": "",
                "forma_de_proprietate": "PROPR.PRIVATA-CAPITAL PRIVAT AUTOHTON",
                "forma_juridica": "SOCIETATE COMERCIALĂ CU RĂSPUNDERE LIMITATĂ",
                "iban": "",
                "e_factura": false,
                "e_factura_data_inregistrare": null,
                "data_actualizare": "2026-08-11",
                "adresa_sediu_social": {
                    "strada": "Str. Memorandumului",
                    "numar": "10",
                    "localitate": "Mun. Cluj-Napoca",
                    "cod_localitate": "103",
                    "judet": "CLUJ",
                    "cod_judet": "CJ",
                    "cod_postal": "400114",
                    "tara": "",
                    "detalii": ""
                },
                "adresa_domiciliu_fiscal": {
                    "strada": "Aleea Nectarului",
                    "numar": "9",
                    "localitate": "Mun. Cluj-Napoca",
                    "cod_localitate": "103",
                    "judet": "CLUJ",
                    "cod_judet": "CJ",
                    "cod_postal": "400683",
                    "tara": "",
                    "detalii": ""
                },
                "tva": {
                    "platitor": true,
                    "perioade": [
                        {
                            "data_inceput_ScpTVA": "2025-09-01",
                            "data_sfarsit_ScpTVA": null,
                            "data_anul_imp_ScpTVA": null,
                            "mesaj_ScpTVA": ""
                        }
                    ]
                },
                "tva_incasare": {
                    "activ": false,
                    "data_inceput": null,
                    "data_sfarsit": null,
                    "tip_act": "",
                    "data_actualizare": null,
                    "data_publicare": null
                },
                "split_tva": {
                    "activ": false,
                    "data_inceput": null,
                    "data_anulare": null
                },
                "status_inactiv": {
                    "inactiv": false,
                    "data_inactivare": null,
                    "data_reactivare": null,
                    "data_publicare": null,
                    "data_radiere": null
                },
                "organ_fiscal": "Administrația Județeană a Finanțelor Publice Cluj",
                "forma_organizare": "PERSOANA JURIDICA",
                "siruta_sediu_social": {
                    "sirsup": 127,
                    "parent_tip": 40,
                    "judet_siruta": 127,
                    "regiune": 6,
                    "med": "1",
                    "judet_denloc": "JUDEŢUL CLUJ",
                    "parent_denloc": "JUDEŢUL CLUJ",
                    "denloc": "MUNICIPIUL CLUJ-NAPOCA",
                    "nuts": "RO113",
                    "parent_siruta": 127,
                    "codp": 0,
                    "jud": 12,
                    "tip": 1,
                    "niv": 2,
                    "siruta": 54975,
                    "fsj": 13,
                    "fsl": "1310911000000"
                },
                "siruta_domiciliu_fiscal": {
                    "sirsup": 127,
                    "parent_tip": 40,
                    "judet_siruta": 127,
                    "regiune": 6,
                    "med": "1",
                    "judet_denloc": "JUDEŢUL CLUJ",
                    "parent_denloc": "JUDEŢUL CLUJ",
                    "denloc": "MUNICIPIUL CLUJ-NAPOCA",
                    "nuts": "RO113",
                    "parent_siruta": 127,
                    "codp": 0,
                    "jud": 12,
                    "tip": 1,
                    "niv": 2,
                    "siruta": 54975,
                    "fsj": 13,
                    "fsl": "1310911000000"
                }
            }
        ],
        "pagination": {
            "total": 1542,
            "page": 1,
            "per_page": 20,
            "total_pages": 78
        }
    },
    "filters_applied": {
        "judet": "CJ",
        "caen": "6201",
        "tva": "1"
    },
    "billing": {
        "results_returned": 20,
        "cost_per_result": 0.7,
        "credits_charged": 14
    }
}
```

| Câmp | Tip | Descriere |
|---|---|---|
| `success` | boolean | true la răspunsurile reușite. |
| `data.items[]` | array | Firmele găsite, cel mult 20. Fiecare element are exact formatul răspunsului de la /firma/{cui}: identificare, adrese, TVA, inactivi, organ fiscal. |
| `data.pagination.total` | integer | Numărul total de firme care corespund filtrelor. |
| `data.pagination.page · per_page · total_pages` | integer | Pagina curentă, mărimea paginii (20) și numărul de pagini. |
| `filters_applied` | object | Filtrele luate în considerare, după validare. |
| `billing.results_returned` | integer | Câte firme s-au taxat. Obiectul billing lipsește când nu s-a taxat nimic. |
| `billing.cost_per_result` | number | Prețul per firmă, în credite generale. |
| `billing.credits_charged` | integer | Creditele consumate de cerere (rotunjite în sus). |

Reguli:

- Costul = numărul de rezultate × 0,70, rotunjit în sus; o căutare fără rezultate e gratuită.
- Firmele cu număr de Registrul Comerțului complet apar înaintea înregistrărilor incomplete la căutarea după denumire.
- Creditele sunt verificate înainte: dacă nu ajung, primești eroare și nu se consumă nimic.
- cod_caen vine fără zeroul din față („111” = 0111) și poate fi din CAEN Rev.2 sau Rev.3: completează cu zero la 4 cifre și ia revizia din /caen/{cui} (caen_versiune) înainte să afișezi denumirea codului.

## 3. Testare fără credite: sandbox

Trimite header-ul `X-Sandbox: true` **fără** cheie API (sau cu o cheie de test care începe cu `sandbox_`). Răspunsurile sunt simulate, nu consumă credite și au header-ul `X-Sandbox-Mode: true`. O cheie reală trimisă împreună cu `X-Sandbox: true` e taxată normal.

| Cerere de test | Endpoint |
|---|---|
| `GET /firme?judet=CJ&caen=6201&tva=1` | Căutare firme |

Datele de test pot diferi în detalii de cele reale: scrie parserul după exemplele din secțiunea 2 (formatul real) și folosește sandboxul pentru fluxul cererii, autentificare, erori și cache.

## 4. Erori

Corpul unei erori: `{ "success": false, "error": "...", "code": "...", "message": "..." }` (câmpul `message` poate lipsi). Decide după statusul HTTP și `code`, nu după text. Nicio eroare nu consumă credite.

| HTTP | code | Când apare | Ce face integrarea | Reîncearcă |
|---|---|---|---|---|
| 401 | AUTH_MISSING_KEY, AUTH_INVALID_KEY, AUTH_KEY_EXPIRED | Cheia lipsește, e greșită sau a expirat. | Eroare de configurare: loghează și verifică FIRMEAPI_KEY. | nu |
| 403 | CREDITS_EXHAUSTED | Creditele nu acoperă cererea. | Mesaj către utilizator cu required_credits și linkul din buy_credits_url. | nu |
| 403 | ACCESS_DENIED_IP, ACCESS_DENIED_SCOPE | Cheia e limitată la alte IP-uri sau endpoint-uri. | Eroare de configurare a cheii, din contul FirmeAPI. | nu |
| 403 | MONTHLY_LIMIT_REACHED | Planul gratuit și-a atins limita lunară de cereri. | Mesaj către utilizator cu linkul din upgrade_url. | nu |
| 422 | VALIDATION_FAILED | Un filtru are format greșit (de ex. data nu e AAAA-LL-ZZ sau pagina depășește limita). | Arată eroarea din errors, pe câmp. Validează înainte de cerere. | nu |
| 429 | LIMIT_EXCEEDED_SECOND | Prea multe cereri într-o secundă. | Așteaptă retry_after secunde și reia cererea. | da |
| 429 | LIMIT_EXCEEDED_DAILY, LIMIT_EXCEEDED_MONTHLY | Limita zilnică sau lunară a planului. | Oprește cererile până la retry_after și anunță utilizatorul. | nu |
| 500 | — | Eroare neașteptată pe server. | Reîncearcă de cel mult 3 ori, la 1, 2 și 4 secunde. | da |

## 5. Reguli de domeniu și de integrare

1. Un câmp `null` înseamnă că sursa nu are informația: nu îl transforma în 0 sau în șir gol.
2. Apelurile se fac din backend; cheia API nu ajunge niciodată în browser sau în aplicația mobilă.
3. Păstrează rezultatele în cache pe cheia cererii; nu repeta aceeași cerere la fiecare afișare.

## 6. Header-e de răspuns utile

| Header | Conținut |
|---|---|
| `X-Credit-Cost` | Creditele consumate de cerere. |
| `X-Credit-Type` | Tipul creditelor folosite de endpoint. |
| `X-Credit-Balance-General` | Soldul de credite generale. |
| `X-Credit-Balance-{TIP}` | Soldul pentru tipul cererii, când nu e general (de exemplu X-Credit-Balance-ACTIONARI). |
| `X-RateLimit-Limit-Second · -Day · -Month` | Limitele planului. |
| `X-RateLimit-Remaining-Second · -Day · -Month` | Cât a mai rămas din fiecare limită. |

## 7. Implementare de referință

TypeScript / Node 18+ (tradu în limbajul proiectului dacă e altul):

```ts
const BASE = "https://www.firmeapi.ro/api/v1";

export class FirmeApiError extends Error {
  constructor(public status: number, public code: string | null, message: string, public body: unknown) {
    super(message);
  }
}

type Query = Record<string, string | number | boolean | undefined>;

async function call<T>(path: string, query?: Query): Promise<T> {
  const url = new URL(BASE + path);
  for (const [k, v] of Object.entries(query ?? {})) if (v !== undefined) url.searchParams.set(k, String(v));

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { "X-API-KEY": process.env.FIRMEAPI_KEY ?? "", Accept: "application/json" }, signal: AbortSignal.timeout(30_000) });
    const body: any = await res.json().catch(() => null);
    if (res.ok) return body as T;

    const code = body?.code ?? null;
    const retriable = res.status >= 500 || code === "LIMIT_EXCEEDED_SECOND";
    if (retriable && attempt < 3) {
      const wait = code === "LIMIT_EXCEEDED_SECOND" ? (body?.retry_after ?? 1) * 1000 : 1000 * 2 ** attempt;
      await new Promise((r) => setTimeout(r, wait));
      continue;
    }
    throw new FirmeApiError(res.status, code, body?.message ?? body?.error ?? res.statusText, body);
  }
}

/** 404 = fără date (netaxat): întoarce null în loc de excepție. */
async function orNull<T>(p: Promise<T>): Promise<T | null> {
  try {
    return await p;
  } catch (e) {
    if (e instanceof FirmeApiError && e.status === 404) return null;
    throw e;
  }
}

// Câte o funcție pentru fiecare endpoint (tipează răspunsurile după câmpurile din secțiunea 2).
export const cautaFirme = (query: Query = {}) => call<any>(`/firme`, query);
```

Echivalent PHP (Laravel):

```php
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;

// config/services.php: 'firmeapi' => ['key' => env('FIRMEAPI_KEY')]
$res = Http::withHeaders(['X-API-KEY' => config('services.firmeapi.key')])->acceptJson()
    ->timeout(30)
    ->retry(3, fn (int $attempt) => 1000 * 2 ** ($attempt - 1),
        fn ($e) => $e instanceof RequestException && $e->response->serverError(), throw: false)
    ->get('https://www.firmeapi.ro/api/v1/firme?judet=CJ&caen=6201&tva=1');

if ($res->status() === 404) return null;   // fără date, netaxat
$res->throw();                              // restul erorilor: vezi secțiunea 4
$data = $res->json();
```

Echivalent Python:

```python
import os, time, requests

BASE = "https://www.firmeapi.ro/api/v1"

def call(path: str, params: dict | None = None):
    for attempt in range(4):
        r = requests.get(BASE + path, params=params, timeout=30,
                         headers={"X-API-KEY": os.environ["FIRMEAPI_KEY"], "Accept": "application/json"})
        if r.status_code == 404:
            return None                      # fără date, netaxat
        retriable = r.status_code >= 500 or (r.status_code == 429 and r.json().get("code") == "LIMIT_EXCEEDED_SECOND")
        if retriable and attempt < 3:
            time.sleep(r.json().get("retry_after", 1) if r.status_code == 429 else 2 ** attempt)
            continue
        r.raise_for_status()
        return r.json()
```

## 8. Teste de acceptanță

1. `cautaFirme` cu cererea `/firme?judet=CJ&caen=6201&tva=1` întoarce 200 și se parsează în câmpurile din secțiunea 2 (sandbox).
2. Un răspuns 404 devine „fără date” (null sau listă goală), fără excepție.
3. Un 503 sau 500 e reîncercat de cel mult 3 ori; un 401 sau 403 nu e reîncercat.
4. Un 429 `LIMIT_EXCEEDED_SECOND` așteaptă `retry_after` secunde și reia cererea.
5. A doua cerere identică vine din cache, fără cerere HTTP.

## 9. Alternativa fără cod: MCP

Dacă utilizatorul vrea ca un asistent AI să interogheze datele direct (nu o integrare în aplicație), folosește serverul MCP: `https://www.firmeapi.ro/api/mcp` cu header-ul `Authorization: Bearer <cheie>`. Tool-uri: `cauta_firma`, cu aceleași costuri. Ghid pe fiecare client: https://www.firmeapi.ro/mcp

## Legături

- Pagina API-ului: https://www.firmeapi.ro/info-api/lista-firme
- Acest ghid (Markdown): https://www.firmeapi.ro/info-api/lista-firme.md
- Specificația OpenAPI: https://www.firmeapi.ro/api/mcp/openapi.json
- Documentația generală: https://www.firmeapi.ro/docs
- Rezumat pentru LLM-uri: https://www.firmeapi.ro/llms.txt
