# FirmeAPI.ro — API Insolvență — Tabelele creditorilor: 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-creditori

Tabelele de creanțe publicate de administratorul judiciar: fiecare creditor cu suma declarată, suma admisă, categoria (garantată, salarială, bugetară, chirografară, subordonată), procentul din masa credală și observațiile. Pentru fiecare dosar indicăm tabelul curent — cel mai recent și cel mai consolidat — ca să nu aduni aceleași datorii de mai multe ori. Sursa: Publicațiile de insolvență (ONRC), extragere automată FirmeAPI. Peste 200.000 de tabele de creanțe, majoritatea din 2024–2026.

## 0. Ce trebuie să livrezi

Integrează în aplicația utilizatorului API-ul Insolvență — Tabelele creditorilor 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 (`bpiCreditori`, `bpiCreditoriCauta`), 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 Tabelele de creanțe ale unei firme — `GET /bpi/creditori/{cui}`

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

Toate tabelele publicate pentru firmă, cu lista completă de creditori, de la cel mai recent. Pentru sume folosește, din fiecare dosar, doar tabelul cel mai recent și cel mai consolidat.

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

Exemplu: `GET https://www.firmeapi.ro/api/v1/bpi/creditori/14231153`

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

```json
{
    "success": true,
    "cui": "14231153",
    "sursa": "Buletinul Procedurilor de Insolvență (ONRC), extragere automată FirmeAPI",
    "total": 7,
    "rezultate": [
        {
            "cui": "14231153",
            "numar_bpi": "15095/2026",
            "numar_dosar": "5244/118/2022",
            "data": "2026-06-08",
            "titlu_document": "Tabel definitiv",
            "tip_tabel": "definitiv consolidat rectificat",
            "extracted_at": "2026-08-14T15:31:53Z",
            "creditori": [
                {
                    "nume": "Creditor exemplu 1 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 2",
                    "categorie": "salariala",
                    "garantii": null,
                    "suma_declarata": 8152,
                    "suma_admisa": 8152,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 3 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 4",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 2681856,
                    "suma_admisa": 2681856,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 5 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 6",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 640000,
                    "suma_admisa": 640000,
                    "procent": null,
                    "observatii": "Observațiile administratorului judiciar."
                }
            ]
        },
        {
            "cui": "14231153",
            "numar_bpi": "14416/2026",
            "numar_dosar": "5244/118/2022",
            "data": "2026-06-02",
            "titlu_document": "Tabel definitiv",
            "tip_tabel": "definitiv consolidat rectificat",
            "extracted_at": "2026-08-14T05:33:05Z",
            "creditori": [
                {
                    "nume": "Creditor exemplu 7 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 8",
                    "categorie": "salariala",
                    "garantii": null,
                    "suma_declarata": 8152,
                    "suma_admisa": 8152,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 9 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 10",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 2681856,
                    "suma_admisa": 2681856,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 11 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 12",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 8428,
                    "suma_admisa": 8428,
                    "procent": null,
                    "observatii": null
                }
            ]
        },
        {
            "cui": "14231153",
            "numar_bpi": "10721/2026",
            "numar_dosar": "5244/118/2022",
            "data": "2026-04-24",
            "titlu_document": "Tabel definitiv",
            "tip_tabel": "suplimentar",
            "extracted_at": "2026-08-13T20:06:10Z",
            "creditori": [
                {
                    "nume": "Creditor exemplu 13 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 14",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 1968507,
                    "suma_admisa": 1968507,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 15 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 16",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 8428,
                    "suma_admisa": 8428,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 17 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 18",
                    "categorie": "chirografara",
                    "garantii": null,
                    "suma_declarata": 5634.24,
                    "suma_admisa": 5634.24,
                    "procent": null,
                    "observatii": null
                }
            ]
        }
    ]
}
```

| 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 debitoare. |
| `rezultate[].numar_bpi` | string | Publicația de insolvență în care a apărut tabelul. |
| `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ță. |
| `rezultate[].data` | date | Data publicării tabelului. |
| `rezultate[].tip_tabel` | string | Tipul tabelului, în formularea sursei: preliminar, definitiv, definitiv consolidat, rectificat, suplimentar etc. |
| `rezultate[].titlu_document` | string | Titlul documentului publicat. |
| `rezultate[].creditori` | array | Rândurile tabelului, câte unul pe creditor. |
| `rezultate[].creditori[].nume` | string | Numele creditorului, cum apare în tabel. |
| `rezultate[].creditori[].nume_key` | string | Cheie de potrivire (cuvintele sortate, fără diacritice). Nu e un identificator: creditorii nu au CUI în tabel. |
| `rezultate[].creditori[].categorie` | string\|null | garantata, salariala, bugetara, chirografara, subordonata, curenta. |
| `rezultate[].creditori[].suma_declarata` | number\|null | Suma cerută de creditor (lei). |
| `rezultate[].creditori[].suma_admisa` | number\|null | Suma admisă de administrator (lei). Diferența față de suma declarată e respinsă. |
| `rezultate[].creditori[].procent` | number\|null | Procentul din totalul masei credale. |
| `rezultate[].creditori[].garantii` | string\|null | Garanțiile menționate. |
| `rezultate[].creditori[].observatii` | string\|null | Observațiile administratorului (creanță sub condiție, tardivă etc.). |
| `rezultate[].extracted_at` | datetime | Momentul extragerii. |

### 2.2 Tabelele de creanțe, cu filtre — `GET /bpi/creditori/lista`

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

Fără filtre, cele mai recent publicate tabele de creanțe. Cu filtrul creditor: la ce firme în insolvență apare un creditor (bancă, furnizor, instituție) — tabelele în care apare și, pentru fiecare, rândurile care se potrivesc (creditori_potriviti).

| Parametru | Unde | Tip | Obligatoriu | Descriere |
|---|---|---|---|---|
| `creditor` | query | string | nu | Numele creditorului căutat (potrivire pe cuvinte). Ex.: `Banca Transilvania`. |
| `categorie` | query | string | nu | Categoria creanței. Valori: `garantata`, `salariala`, `bugetara`, `chirografara`, `subordonata`, `curenta`. |
| `suma_min` | query | number | nu | Suma admisă minimă pe rândul creditorului (lei). Ex.: `100000`. |
| `cui` | query | string | nu | Doar tabelele unei firme debitoare. |
| `numar_dosar` | query | string | nu | Dosarul de insolvență. |
| `tip_tabel` | query | string | nu | Tipul tabelului, ca în sursă (ex. preliminar, definitiv, definitiv consolidat). |
| `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`. |
| `pagina` | query | integer | nu | Numărul paginii, de la 1. Ex.: `1`. |
| `per_pagina` | query | integer | nu | Rezultate pe pagină, maxim 20. Ex.: `20`. |
| `ordine` | query | string | nu | Ordinea rezultatelor (vezi valorile permise pe endpoint). Valori: `recente`, `vechi`. Ex.: `recente`. |

Exemplu: `GET https://www.firmeapi.ro/api/v1/bpi/creditori/lista?creditor=Banca%20Transilvania&suma_min=100000`

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": 8213,
    "total_pagini": 4107,
    "rezultate": [
        {
            "cui": "44879816",
            "numar_bpi": "23287/2026",
            "numar_dosar": "18/1285/2025",
            "data": "2026-09-24",
            "titlu_document": "Tabel definitiv rectificat / completat / actualizat al creanţelor, nr. 4, din data de 23-09-2026, Tribunalul Specializat Cluj",
            "tip_tabel": "definitiv actualizat",
            "extracted_at": "2026-09-24T08:15:25Z",
            "creditori": [
                {
                    "nume": "Creditor exemplu 1 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 2",
                    "categorie": "garantata",
                    "garantii": null,
                    "suma_declarata": 190958.04,
                    "suma_admisa": 190958.04,
                    "procent": 50.07,
                    "observatii": "Observațiile administratorului judiciar."
                },
                {
                    "nume": "Creditor exemplu 3 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 4",
                    "categorie": "bugetara",
                    "garantii": null,
                    "suma_declarata": 80027,
                    "suma_admisa": 80027,
                    "procent": 20.98,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 5 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 6",
                    "categorie": "chirografara",
                    "garantii": null,
                    "suma_declarata": 1820.93,
                    "suma_admisa": 1820.93,
                    "procent": 0.48,
                    "observatii": null
                }
            ],
            "creditori_potriviti": [
                {
                    "nume": "Creditor exemplu 7 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 8",
                    "categorie": "garantata",
                    "garantii": null,
                    "suma_declarata": 190958.04,
                    "suma_admisa": 190958.04,
                    "procent": 50.07,
                    "observatii": "Observațiile administratorului judiciar."
                }
            ]
        },
        {
            "cui": "33781242",
            "numar_bpi": "23207/2026",
            "numar_dosar": "",
            "data": "2026-09-23",
            "titlu_document": "Tabel definitiv rectificat / completat / actualizat al creanţelor, nr. 1463, din data de 21-09-2026, Tribunalul Mehedinţi, Secţia a II-a Civilă, de Contencios Administrativ şi Fiscal",
            "tip_tabel": "definitiv rectificativ",
            "extracted_at": "2026-09-23T12:30:25Z",
            "creditori": [
                {
                    "nume": "Creditor exemplu 9 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 10",
                    "categorie": "garantata",
                    "garantii": "Art. 159 alin. (1) pct. 3 Legea 85/2014",
                    "suma_declarata": null,
                    "suma_admisa": 118853.79,
                    "procent": null,
                    "observatii": "Observațiile administratorului judiciar."
                },
                {
                    "nume": "Creditor exemplu 11 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 12",
                    "categorie": "garantata",
                    "garantii": "Art. 159 alin. (1) pct. 3 Legea 85/2014",
                    "suma_declarata": null,
                    "suma_admisa": 243000,
                    "procent": null,
                    "observatii": null
                },
                {
                    "nume": "Creditor exemplu 13 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 14",
                    "categorie": "garantata",
                    "garantii": "Art. 159 alin. (1) pct. 3 Legea 85/2014",
                    "suma_declarata": null,
                    "suma_admisa": 86062,
                    "procent": null,
                    "observatii": null
                }
            ],
            "creditori_potriviti": [
                {
                    "nume": "Creditor exemplu 15 SRL",
                    "nume_key": "CREDITOR EXEMPLU SRL 16",
                    "categorie": "chirografara",
                    "garantii": null,
                    "suma_declarata": null,
                    "suma_admisa": 176233.88,
                    "procent": null,
                    "observatii": "Observațiile administratorului judiciar."
                }
            ]
        }
    ],
    "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 debitoare. |
| `rezultate[].numar_bpi` | string | Publicația de insolvență în care a apărut tabelul. |
| `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ță. |
| `rezultate[].data` | date | Data publicării tabelului. |
| `rezultate[].tip_tabel` | string | Tipul tabelului, în formularea sursei: preliminar, definitiv, definitiv consolidat, rectificat, suplimentar etc. |
| `rezultate[].titlu_document` | string | Titlul documentului publicat. |
| `rezultate[].creditori` | array | Rândurile tabelului, câte unul pe creditor. |
| `rezultate[].creditori[].nume` | string | Numele creditorului, cum apare în tabel. |
| `rezultate[].creditori[].nume_key` | string | Cheie de potrivire (cuvintele sortate, fără diacritice). Nu e un identificator: creditorii nu au CUI în tabel. |
| `rezultate[].creditori[].categorie` | string\|null | garantata, salariala, bugetara, chirografara, subordonata, curenta. |
| `rezultate[].creditori[].suma_declarata` | number\|null | Suma cerută de creditor (lei). |
| `rezultate[].creditori[].suma_admisa` | number\|null | Suma admisă de administrator (lei). Diferența față de suma declarată e respinsă. |
| `rezultate[].creditori[].procent` | number\|null | Procentul din totalul masei credale. |
| `rezultate[].creditori[].garantii` | string\|null | Garanțiile menționate. |
| `rezultate[].creditori[].observatii` | string\|null | Observațiile administratorului (creanță sub condiție, tardivă etc.). |
| `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 20 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. NU aduna tabelele între ele: un dosar își republică tabelul (preliminar, definitiv, consolidat, rectificat) și fiecare repetă aceleași datorii. Ia un singur tabel per dosar: cel mai recent, iar la aceeași dată cel mai consolidat (definitiv consolidat > definitiv > preliminar).
3. Creditorii nu au cod fiscal în tabele, deci potrivirea se face după nume. Două direcții fiscale locale pot avea chei apropiate.
4. Sumele sunt în lei, cum apar în tabel.
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 bpiCreditori = (cui: string) => orNull(call<any>(`/bpi/creditori/${normalizeCui(cui)}`));
export const bpiCreditoriCauta = (query: Query = {}) => call<any>(`/bpi/creditori/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/creditori/14231153');

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. `bpiCreditori` cu cererea `/bpi/creditori/14231153` întoarce 200 și se parsează în câmpurile din secțiunea 2 (fixture).
2. `bpiCreditoriCauta` cu cererea `/bpi/creditori/lista?creditor=Banca%20Transilvania&suma_min=100000` î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_creditori`, `bpi_creditori_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-creditori
- Acest ghid (Markdown): https://www.firmeapi.ro/info-api/bpi-creditori.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
