# FirmeAPI.ro — API Firme Noi: 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/firme-noi

Firmele înregistrate la Registrul Comerțului în fiecare zi, cu profilul complet și filtre după județ și localitate. Abonament fix, cereri nelimitate, fără credite.

## 0. Ce trebuie să livrezi

Integrează în aplicația utilizatorului API-ul Firme Noi 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 (`firmeNoi`), 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** — cu răspunsuri simulate din acest ghid (mock HTTP), 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 Firmele înregistrate într-o zi — `GET /firme-noi`

Cost: **abonament fix** per cerere reușită (2xx). Erorile nu se taxează.

Firmele înregistrate în ziua cerută, paginat. Cererile nu consumă credite.

| Parametru | Unde | Tip | Obligatoriu | Descriere |
|---|---|---|---|---|
| `data` | query | AAAA-LL-ZZ | nu | Ziua înregistrării. Implicit, ultima zi lucrătoare. Nu poate fi înainte de data abonamentului. Ex.: `2026-09-23`. |
| `judet` | query | string | nu | Denumirea județului, exact ca în adresa sediului. Ex.: `CLUJ`. |
| `localitate` | query | string | nu | Denumirea localității, exact ca în adresa sediului. Ex.: `Mun. Cluj-Napoca`. |
| `page` | query | integer | nu | Pagina, de la 1; 25 de firme pe pagină. |

Exemplu: `GET https://www.firmeapi.ro/api/v1/firme-noi?data=2026-09-23&judet=CLUJ`

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

```json
{
    "success": true,
    "data": {
        "companies": [
            {
                "cui": 48271635,
                "denumire": "NOVA SOLUTIONS SRL",
                "data_inregistrare": "2026-09-23",
                "stare": "INREGISTRAT din data 23.09.2026",
                "cod_caen": "6201",
                "nr_reg_com": "J12/1045/2026",
                "telefon": "0212345678",
                "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": "Aleea Nectarului",
                    "numar": "9",
                    "localitate": "Mun. Cluj-Napoca",
                    "cod_localitate": "103",
                    "judet": "CLUJ",
                    "cod_judet": "CJ",
                    "cod_postal": "400683",
                    "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": false,
                    "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"
                }
            }
        ],
        "total": 38,
        "date": "2026-09-23",
        "pagination": {
            "page": 1,
            "per_page": 25,
            "total_pages": 2
        }
    },
    "subscription_start": "2026-09-01"
}
```

| Câmp | Tip | Descriere |
|---|---|---|
| `success` | boolean | true la răspunsurile reușite. |
| `data.companies[]` | array | Firmele înregistrate în ziua cerută (doar cele cu număr J sau F la Registrul Comerțului). Fiecare are formatul de la /firma/{cui}: identificare, adrese, TVA, inactivitate, organ fiscal, SIRUTA. |
| `data.total` | integer | Numărul firmelor înregistrate în acea zi, după filtre. |
| `data.date` | string | Ziua pentru care s-au întors firmele. |
| `data.pagination` | object | page, per_page (25), total_pages. |
| `subscription_start` | string | Data de la care ai acces la date. |

Reguli:

- Accesul începe din ziua activării abonamentului; zilele anterioare întorc 403 DATE_BEFORE_SUBSCRIPTION.
- Rulează sincronizarea o dată pe zi, după ce ziua s-a încheiat, și parcurge toate paginile.
- 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

Endpoint-urile acestui API nu au sandbox. Pentru testele automate, folosește răspunsurile din secțiunea 2 ca fixture (mock HTTP pe clientul tău). Pentru o verificare reală, fă o singură cerere cu cheia utilizatorului: erorile și răspunsurile 404 nu consumă credite.

## 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 API lipsește, e greșită, dezactivată sau expirată. Trimite-o în header-ul X-API-KEY (sau ca Bearer token). | Eroare de configurare: loghează și verifică FIRMEAPI_KEY. | nu |
| 403 | FIRME_NOI_PLAN_REQUIRED | Endpoint-ul e disponibil doar cu abonamentul Firme Noi. | Mesaj către utilizator, cu linkul din upgrade_url când există. | nu |
| 403 | DATE_BEFORE_SUBSCRIPTION | Ai cerut o zi de dinainte de activarea abonamentului. | Mesaj către utilizator, cu linkul din upgrade_url când există. | nu |
| 403 | FIRME_NOI_ONLY | Cu planul Firme Noi nu ai acces la celelalte endpoint-uri. | Mesaj către utilizator, cu linkul din upgrade_url când există. | nu |
| 422 | VALIDATION_FAILED | Data nu are formatul AAAA-LL-ZZ sau pagina e în afara limitei. | Arată eroarea din errors, pe câmp. Validează înainte de cerere. | nu |
| 429 | LIMIT_EXCEEDED_SECOND, _DAILY, _MONTHLY | Ai depășit limita de cereri pe secundă, zi sau lună a planului. Câmpul retry_after spune după câte secunde reîncerci. | Așteaptă retry_after secunde (sau până a doua zi, la limita zilnică) și reia cererea. | da |
| 500 | — | Eroare la căutare (error: SearchError). | 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.

## 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 firmeNoi = (query: Query = {}) => call<any>(`/firme-noi`, 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-noi?data=2026-09-23&judet=CLUJ');

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. `firmeNoi` cu cererea `/firme-noi?data=2026-09-23&judet=CLUJ` întoarce 200 și se parsează în câmpurile din secțiunea 2 (fixture).
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.

## Legături

- Pagina API-ului: https://www.firmeapi.ro/info-api/firme-noi
- Acest ghid (Markdown): https://www.firmeapi.ro/info-api/firme-noi.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
