# FirmeAPI.ro — API Insolvență — Evenimente: 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/bpi-evenimente

Fiecare publicație de insolvență citită și transformată într-un eveniment structurat: tipul actului (citație, raport, tabel de creanțe, sentință, vânzare), data actului, stadiul procedurii după el (observație, reorganizare, faliment, închisă), practicianul în insolvență, adunarea creditorilor și următorul termen. Rândurile unei firme, în ordinea datei, sunt istoria completă a insolvenței ei. Sursa: Publicațiile de insolvență (ONRC), extragere automată FirmeAPI. Peste 100.000 de evenimente; publicațiile noi sunt citite în câteva ore. Istoricul vechi e acoperit parțial: lipsa unui eveniment nu dovedește că nu a existat.

## 0. Ce trebuie să livrezi

Integrează în aplicația utilizatorului API-ul Insolvență — Evenimente 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 (`bpiEvenimente`, `bpiEvenimenteCauta`), 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”.
- Endpoint-urile folosesc **credite premium**, disponibile cu abonament plătit sau pachet premium.
- 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 Cronologia insolvenței unei firme — `GET /bpi/evenimente/{cui}`

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

Toate evenimentele extrase pentru firmă, de la cel mai recent. Primul rând care precizează stadiul dă stadiul curent al procedurii, iar câmpul termen_urmator dă termenele cunoscute.

| Parametru | Unde | Tip | Obligatoriu | Descriere |
|---|---|---|---|---|
| `cui` | cale | string | da | Codul fiscal (CUI) al firmei, fără RO. Ex.: `28557746`. |

Exemplu: `GET https://www.firmeapi.ro/api/v1/bpi/evenimente/28557746`

Răspuns 200 (răspuns real, scurtat: listele la câteva elemente, fără nume de persoane fizice):

```json
{
    "success": true,
    "cui": "28557746",
    "sursa": "Buletinul Procedurilor de Insolvență (ONRC), extragere automată FirmeAPI",
    "total": 3,
    "rezultate": [
        {
            "cui": "28557746",
            "numar_bpi": "22916/2026",
            "numar_dosar": "36697/3/2025",
            "data": "2026-09-21",
            "titlu_document": "Convocare adunare creditori, nr. 17452, din data de 21-09-2026, Tribunalul Bucureşti, Secţia a VII-a Civilă",
            "tip_eveniment": "convocare_adunare",
            "stadiu": null,
            "data_eveniment": "2026-09-21",
            "termen_urmator": "2026-10-23",
            "practician": null,
            "adunare": null,
            "sentinta": null,
            "plan_status": null,
            "sume": null,
            "rezumat": null,
            "extracted_at": "2026-09-21T13:41:38Z"
        },
        {
            "cui": "28557746",
            "numar_bpi": "20344/2026",
            "numar_dosar": "36697/3/2025/a3",
            "data": "2026-08-14",
            "titlu_document": "Notificare",
            "tip_eveniment": "atragere_raspundere",
            "stadiu": "faliment",
            "data_eveniment": "2026-06-12",
            "termen_urmator": null,
            "practician": {
                "nume": "Cabinet Individual de Insolvență Exemplu",
                "rfo": null
            },
            "adunare": null,
            "sentinta": {
                "solutie": "Respinge ca neîntemeiată cererea formulată de reclamantul CII CLEJA ALINA SINZIANA, în contradictoriu cu pârâtul POPESCU NICULAE",
                "art169_parati": [
                    "Persoană atrasă la răspundere"
                ],
                "suma": null
            },
            "plan_status": null,
            "sume": null,
            "rezumat": "Tribunalul București a respins ca neîntemeiată cererea lichidatorului judiciar de atragere la răspundere a administratorului Popescu Niculae pentru pasivul neacoperit al debitorului.",
            "extracted_at": "2026-08-26T18:24:18Z"
        },
        {
            "cui": "28557746",
            "numar_bpi": "7180/2026",
            "numar_dosar": "36697/3/2025",
            "data": "2026-03-17",
            "titlu_document": "Încheiere, din data de 16-03-2026, Tribunalul Bucureşti, Secţia a VII-a Civilă",
            "tip_eveniment": "comunicare_sentinta",
            "stadiu": "faliment",
            "data_eveniment": "2026-03-16",
            "termen_urmator": null,
            "practician": {
                "nume": "Cabinet Individual de Insolvență Exemplu",
                "rfo": null
            },
            "adunare": null,
            "sentinta": {
                "solutie": "Respinge cererea de prelungire a termenului prevăzut de art. 102 alin. 1^1 din Legea nr. 85/2014, în cadrul procedurii falimentului ALEXANDRA GARDEN CONSTRUCT SRL, formulată de Direcția Generala Regionala a Finanțelor…",
                "art169_parati": [],
                "suma": null
            },
            "plan_status": null,
            "sume": null,
            "rezumat": "Judecătorul-sindic a respins cererea DGRFP București de prelungire a termenului de inspecție fiscală în falimentul ALEXANDRA GARDEN CONSTRUCT SRL, ca neîntemeiată.",
            "extracted_at": "2026-08-25T06:21:25Z"
        }
    ]
}
```

| Câmp | Tip | Descriere |
|---|---|---|
| `success` | boolean | true când cererea a reușit. |
| `cui` | string | Valoarea cerută, normalizată. |
| `sursa` | string | Sursa datelor. |
| `total` | integer | Câte rezultate există pentru cheie. |
| `afisate` | integer | Doar când total e mai mare decât limita: câte sunt întoarse. |
| `nota` | string | Doar când rezultatele sunt trunchiate: cum obții restul. |
| `rezultate` | array | Rezultatele, de la cel mai recent. |
| `rezultate[].cui` | string | CUI-ul firmei aflate în insolvență. |
| `rezultate[].numar_bpi` | string | Numărul publicației de insolvență (ex. 7180/2026) — identifică documentul sursă. |
| `rezultate[].are_pdf` | bool | Dacă avem PDF-ul original al publicației. Sursele sunt disponibile pentru publicațiile din 2026; restul se procesează. |
| `rezultate[].sursa_pdf` | string\|null | Adresa de la care se ia PDF-ul original: GET /api/v1/bpi/{cui}/{numar_bpi}/sursa. Costă 3 credite premium per fișier livrat; verificarea de aici nu costă nimic. |
| `rezultate[].numar_dosar` | string\|null | Dosarul de insolvență de la tribunal. Lipsește la aproximativ 2% din rânduri. |
| `rezultate[].data` | date | Data PUBLICĂRII documentului. |
| `rezultate[].data_eveniment` | date\|null | Data actului în sine (sentința, raportul) — nu data publicării. |
| `rezultate[].tip_eveniment` | string | Tipul actului, vocabular închis (vezi valorile filtrului tip_eveniment). |
| `rezultate[].stadiu` | string\|null | Stadiul decis de instanță după act: observatie, reorganizare, faliment, inchisa. Null când documentul nu îl precizează. |
| `rezultate[].termen_urmator` | date\|null | Următorul termen stabilit de document: ședință, adunarea creditorilor, licitație. |
| `rezultate[].practician` | object\|null | Practicianul în insolvență: nume și rfo (numărul din registrul profesional). |
| `rezultate[].adunare` | object\|null | Pentru convocări și procese-verbale: data, ora și ordinea de zi (ordine_zi[]). |
| `rezultate[].sentinta` | object\|null | Pentru hotărâri: solutie (dispozitivul), art169_parati[] (persoanele atrase la răspundere), suma. |
| `rezultate[].plan_status` | string\|null | Planul de reorganizare: depus, votat sau respins. |
| `rezultate[].sume` | object\|null | Doar pentru rapoartele de distribuire: distribuit și fonduri. |
| `rezultate[].rezumat` | string\|null | Rezumatul documentului într-o frază, pentru afișare. |
| `rezultate[].titlu_document` | string | Titlul original al documentului publicat. |
| `rezultate[].extracted_at` | datetime | Momentul extragerii. |

### 2.2 Caută evenimente de insolvență — `GET /bpi/evenimente/lista`

Cost: **1 credit / rezultat premium** per cerere reușită (2xx). Erorile nu se taxează.

Evenimente din toate dosarele, filtrate după tip, stadiu, perioadă, termen, dosar sau practician. Util pentru alerte: „ce firme au termen în următoarele 14 zile” sau „ce proceduri s-au deschis săptămâna aceasta”.

| Parametru | Unde | Tip | Obligatoriu | Descriere |
|---|---|---|---|---|
| `cui` | query | string | nu | Doar evenimentele unei firme. Ex.: `28557746`. |
| `tip_eveniment` | query | string | nu | Tipul actului; mai multe valori separate prin virgulă. Valori: `raport_activitate`, `citatie`, `proces_verbal_adunare`, `convocare_adunare`, `tabel_creante`, `notificare_deschidere`, `comunicare_sentinta`, `raport_cauze`, `sentinta_inchidere`, `propunere_inchidere`, `raport_fonduri_distribuire`, `atragere_raspundere`, `publicatie_vanzare`, `propunere_faliment`, `incheiere_amanare`, `plan_reorganizare`, `altele`. Ex.: `notificare_deschidere`. |
| `stadiu` | query | string | nu | Stadiul procedurii după act. Valori: `observatie`, `reorganizare`, `faliment`, `inchisa`. |
| `numar_dosar` | query | string | nu | Numărul dosarului de insolvență. Ex.: `36697/3/2025`. |
| `practician` | query | string | nu | Numele practicianului în insolvență (administrator/lichidator judiciar). |
| `q` | query | string | nu | Text liber în rezumat și titlul documentului. |
| `data_de` | query | date (AAAA-LL-ZZ) | nu | De la această dată (data ≥), format AAAA-LL-ZZ. Ex.: `2026-01-01`. |
| `data_pana` | query | date (AAAA-LL-ZZ) | nu | Până la această dată (data ≤), format AAAA-LL-ZZ. Ex.: `2026-12-31`. |
| `termen_de` | query | date (AAAA-LL-ZZ) | nu | De la această dată (termen_urmator ≥), format AAAA-LL-ZZ. Ex.: `2026-01-01`. |
| `termen_pana` | query | date (AAAA-LL-ZZ) | nu | Până la această dată (termen_urmator ≤), format AAAA-LL-ZZ. Ex.: `2026-12-31`. |
| `pagina` | query | integer | nu | Numărul paginii, de la 1. Ex.: `1`. |
| `per_pagina` | query | integer | nu | Rezultate pe pagină, maxim 50. Ex.: `20`. |
| `ordine` | query | string | nu | Ordinea rezultatelor (vezi valorile permise pe endpoint). Valori: `recente`, `vechi`, `termen`. Ex.: `recente`. |

Exemplu: `GET https://www.firmeapi.ro/api/v1/bpi/evenimente/lista?tip_eveniment=notificare_deschidere&data_de=2026-09-01`

Răspuns 200 (răspuns real, scurtat: listele la câteva elemente, fără nume de persoane fizice):

```json
{
    "success": true,
    "sursa": "Buletinul Procedurilor de Insolvență (ONRC), extragere automată FirmeAPI",
    "pagina": 1,
    "per_pagina": 2,
    "total": 957,
    "total_pagini": 479,
    "rezultate": [
        {
            "cui": "5737047",
            "numar_bpi": "23177/2026",
            "numar_dosar": "786/98/2019",
            "data": "2026-09-23",
            "titlu_document": "Comunicare sentinţă, nr. 823, din data de 11-09-2026, Tribunalul Ialomiţa, Secţia Civilă",
            "tip_eveniment": "notificare_deschidere",
            "stadiu": "faliment",
            "data_eveniment": "2026-09-11",
            "termen_urmator": "2026-10-20",
            "practician": {
                "nume": "Cabinet Individual de Insolvență Exemplu",
                "rfo": null
            },
            "adunare": null,
            "sentinta": {
                "solutie": "Admite cererea administratorului judiciar și dispune intrarea debitoarei OYL COMPANY SRL în procedura generală a falimentului, dizolvarea societății, ridicarea dreptului de administrare și desemnarea C.I.I. RĂCEANU VI…",
                "art169_parati": [],
                "suma": null
            },
            "plan_status": null,
            "sume": null,
            "rezumat": "Tribunalul Ialomița a admis cererea administratorului judiciar și a dispus intrarea în faliment a debitoarei OYL COMPANY SRL, cu desemnarea C.I.I. Răceanu Vica ca lichidator judiciar și termen limită de depunere a cre…",
            "extracted_at": "2026-09-23T13:40:42Z"
        },
        {
            "cui": "37147095",
            "numar_bpi": "23180/2026",
            "numar_dosar": "158/1259/2026",
            "data": "2026-09-23",
            "titlu_document": "Notificare, Tribunalul Specializat Argeş",
            "tip_eveniment": "notificare_deschidere",
            "stadiu": "observatie",
            "data_eveniment": "2026-09-03",
            "termen_urmator": "2026-09-24",
            "practician": {
                "nume": "Cabinet Individual de Insolvență Exemplu",
                "rfo": null
            },
            "adunare": null,
            "sentinta": {
                "solutie": "Admite cererile creditorilor si dispune deschiderea procedurii insolventei in forma generala a debitorului Agromas Prod Team SRL, mentine dreptul de administrare si desemneaza administrator judiciar provizoriu Enso SP…",
                "art169_parati": [],
                "suma": null
            },
            "plan_status": null,
            "sume": null,
            "rezumat": "Tribunalul Specializat Arges a deschis procedura generala de insolventa a Agromas Prod Team SRL, mentinand dreptul de administrare si desemnand administrator judiciar provizoriu Enso SPRL Filiala Dolj.",
            "extracted_at": "2026-09-23T13:40:46Z"
        }
    ],
    "cost": {
        "credite_consumate": 2,
        "mod": "per_rezultat",
        "tip": "premium"
    }
}
```

| Câmp | Tip | Descriere |
|---|---|---|
| `success` | boolean | true când cererea a reușit. |
| `sursa` | string | Sursa datelor. |
| `pagina · per_pagina` | integer | Pagina curentă și rezultatele pe pagină. |
| `total · total_pagini` | integer | Totalul rezultatelor care se potrivesc filtrelor și numărul de pagini pe care le poți parcurge. |
| `rezultate` | array | Rezultatele paginii. |
| `rezultate[].cui` | string | CUI-ul firmei aflate în insolvență. |
| `rezultate[].numar_bpi` | string | Numărul publicației de insolvență (ex. 7180/2026) — identifică documentul sursă. |
| `rezultate[].are_pdf` | bool | Dacă avem PDF-ul original al publicației. Sursele sunt disponibile pentru publicațiile din 2026; restul se procesează. |
| `rezultate[].sursa_pdf` | string\|null | Adresa de la care se ia PDF-ul original: GET /api/v1/bpi/{cui}/{numar_bpi}/sursa. Costă 3 credite premium per fișier livrat; verificarea de aici nu costă nimic. |
| `rezultate[].numar_dosar` | string\|null | Dosarul de insolvență de la tribunal. Lipsește la aproximativ 2% din rânduri. |
| `rezultate[].data` | date | Data PUBLICĂRII documentului. |
| `rezultate[].data_eveniment` | date\|null | Data actului în sine (sentința, raportul) — nu data publicării. |
| `rezultate[].tip_eveniment` | string | Tipul actului, vocabular închis (vezi valorile filtrului tip_eveniment). |
| `rezultate[].stadiu` | string\|null | Stadiul decis de instanță după act: observatie, reorganizare, faliment, inchisa. Null când documentul nu îl precizează. |
| `rezultate[].termen_urmator` | date\|null | Următorul termen stabilit de document: ședință, adunarea creditorilor, licitație. |
| `rezultate[].practician` | object\|null | Practicianul în insolvență: nume și rfo (numărul din registrul profesional). |
| `rezultate[].adunare` | object\|null | Pentru convocări și procese-verbale: data, ora și ordinea de zi (ordine_zi[]). |
| `rezultate[].sentinta` | object\|null | Pentru hotărâri: solutie (dispozitivul), art169_parati[] (persoanele atrase la răspundere), suma. |
| `rezultate[].plan_status` | string\|null | Planul de reorganizare: depus, votat sau respins. |
| `rezultate[].sume` | object\|null | Doar pentru rapoartele de distribuire: distribuit și fonduri. |
| `rezultate[].rezumat` | string\|null | Rezumatul documentului într-o frază, pentru afișare. |
| `rezultate[].titlu_document` | string | Titlul original al documentului publicat. |
| `rezultate[].extracted_at` | datetime | Momentul extragerii. |
| `cost.credite_consumate · mod · tip` | integer · string | Creditele consumate de cerere, modul de taxare (fix sau per_rezultat) și tipul creditelor (general sau premium). |

Reguli:

- Maximum 50 rezultate pe pagină. Paginile acoperă toate rezultatele care se potrivesc filtrelor; dacă o căutare e prea largă pentru a fi paginată până la capăt, răspunsul spune până unde poți merge.
- Costul se calculează pe numărul de rezultate întoarse; o căutare fără rezultate nu costă nimic.

## 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 lipsește, e greșită sau a expirat. | Eroare de configurare: loghează și verifică FIRMEAPI_KEY. | nu |
| 403 | PREMIUM_INSUFFICIENT_CREDITS | Rezultatele paginii costă mai mult decât creditele disponibile. | Răspunsul spune costul cererii și soldul: cere mai puține rezultate (per_pagina mai mic) sau cumpără credite. | nu |
| 403 | PREMIUM_REQUIRES_PAID_SUBSCRIPTION | Contul nu are abonament plătit. | Mesaj către utilizator cu linkul din upgrade_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 |
| 404 | — | Nu există date pentru cheia cerută. | Afișează „fără date”. Păstrează în cache scurt (de exemplu o oră). | nu |
| 422 | VALIDATION_FAILED | Un filtru invalid sau lipsește un parametru obligatoriu. | 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. Important când interpretezi datele: PDF-ul original al publicației se ia separat, cu GET /api/v1/bpi/{cui}/{numar_bpi}/sursa: 3 credite premium per fișier livrat, fiecare descărcare se taxează. Sursele sunt disponibile pentru publicațiile din 2026; pentru restul, câmpul are_pdf e false și documentele se procesează.
2. Stadiul este ce a decis INSTANȚA, nu ce cere documentul: un plan depus nu înseamnă reorganizare, iar o propunere de faliment nu înseamnă faliment.
3. Nu număra firmele după stadiu adunând toate rândurile — o firmă cu 30 de publicații ar conta de 30 de ori. Folosește stadiul din cel mai recent eveniment al firmei, din răspunsul /bpi/evenimente/{cui}.
4. Null înseamnă „documentul nu spune”, nu „necunoscut”. Nu completa golurile cu evenimentul anterior fără să marchezi asta.
5. Normalizează CUI-ul înainte de cerere: elimină prefixul RO, spațiile și orice caracter care nu e cifră.
6. Un câmp `null` înseamnă că sursa nu are informația: nu îl transforma în 0 sau în șir gol.
7. Apelurile se fac din backend; cheia API nu ajunge niciodată în browser sau în aplicația mobilă.
8. 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;
  }
}

export const normalizeCui = (input: string) => input.replace(/^\s*RO/i, "").replace(/\D/g, "");

// Câte o funcție pentru fiecare endpoint (tipează răspunsurile după câmpurile din secțiunea 2).
export const bpiEvenimente = (cui: string) => orNull(call<any>(`/bpi/evenimente/${normalizeCui(cui)}`));
export const bpiEvenimenteCauta = (query: Query = {}) => call<any>(`/bpi/evenimente/lista`, 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/bpi/evenimente/28557746');

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. `bpiEvenimente` cu cererea `/bpi/evenimente/28557746` întoarce 200 și se parsează în câmpurile din secțiunea 2 (fixture).
2. `bpiEvenimenteCauta` cu cererea `/bpi/evenimente/lista?tip_eveniment=notificare_deschidere&data_de=2026-09-01` întoarce 200 și se parsează în câmpurile din secțiunea 2 (fixture).
3. Un CUI scris `RO 12345678` e normalizat la `12345678` înainte de cerere.
4. Un răspuns 404 devine „fără date” (null sau listă goală), fără excepție.
5. Un 503 sau 500 e reîncercat de cel mult 3 ori; un 401 sau 403 nu e reîncercat.
6. Un 429 `LIMIT_EXCEEDED_SECOND` așteaptă `retry_after` secunde și reia cererea.
7. 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: `bpi_evenimente`, `bpi_evenimente_cauta`, cu aceleași costuri. Ghid pe fiecare client: https://www.firmeapi.ro/mcp

## Legături

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