> Source: https://fiscal.link/payment-terminals/docs/api
> Rutele /api/v2/payment-terminals ale API-ului local FiscalLink: plată, anulare, închidere de zi, test, stări, coduri de eroare și diferențe între terminale. Urmează să apară.

# API pentru integratori

Consultă rutele API pentru încasări cu cardul, parametrii requesturilor și formatul răspunsurilor. Ghidul explică și stările tranzacțiilor, erorile și diferențele dintre terminale.

Urmează să apară

Plata cu cardul pe terminalele bancare vine într-o versiune viitoare a aplicației FiscalLink. Nu e încă disponibilă: pagina descrie cum va funcționa.

## Adresă și acces

Rutele terminalelor fac parte din [API-ul local](https://fiscal.link/integrators/docs/local), versiunea 2, la aceeași adresă: `https://localhost.fiscallink.ro:65401/api/v2` sau `http://{ip-local}:65400/api/v2`.

-   **Doar pe v2.** Rutele sunt sub `/api/v2/payment-terminals`. Versiunea 1 (`/api`) nu le are: `/api/payment-terminals` răspunde `404`.
-   **Fără cheie.** Nu trimiți nicio credențială. Fără licență pe casa conectată, orice rută răspunde `401`.
-   **Casa de marcat conectată.** Plata, anularea, închiderea de zi și testul cer o casă conectată și licențiată; altfel răspund `424` `cash_register_required`. Citirile (lista, detaliile, o tranzacție) merg și fără casă. Când casa se deconectează, terminalele se deconectează după operațiunea în curs, niciodată în mijlocul unei plăți.
-   **Formatul.** La succes: `{ "statusCode": 200, "data": … }`, câmpuri camelCase. La eroare: `statusCode`, `message` (în limba aplicației) și `code`, un identificator stabil; unele erori au și câmpuri în plus. Ramifică după `code`, nu după text.
-   **Doar lei.** Sumele sunt în RON, cu cel mult două zecimale.
-   **Configurarea e în aplicație.** Terminalele se adaugă, se modifică și se șterg doar din aplicație; API-ul le folosește.

## Ce poate fiecare terminal

Rutele sunt comune tuturor terminalelor, dar operațiunile disponibile depind de protocol. Pentru operațiunile marcate „Nu”, API-ul returnează codul indicat în tabel.

| Protocol (`protocol`) | Test | Plată | Anulare | Închidere de zi | Verificare automată |
| --- | --- | --- | --- | --- | --- |
| SmartPay (`SmartPay`) | Întreabă terminalul | Da | Da | Da: număr, total, lot | Da |
| GPE (`Gpe`) | Întreabă terminalul | Da | Da (reversare) | Da: număr, total | Da |
| Ingenio (`Ingenio`) | Întreabă terminalul | Da | Da | Da: lot | Nu |
| Mellon (`Mellon`) | Doar configurația | Da | Nu (`409` `transaction_not_voidable`) | Nu (`500` `internal_error`) | Nu |
| Payten (`Payten`, `PaytenBrd`) | Doar configurația | Da | Da | Da: număr, total, lot | Nu |
| ePay (`Epay`) | Întreabă terminalul | Da | Nu (`409` `transaction_not_voidable`) | Da, fără totaluri | Nu |
| Printec (`Printec`, `PrintecDll`) | Doar configurația | Da | Da | Nu (`500` `internal_error`) | Nu |
| FIP11 (`Fip11`) | Doar configurația | Da | Da | O pornește; nu primești totaluri | Nu |

-   **Test.** „Întreabă terminalul”: aplicația îi cere terminalului seria și, unde o dă, versiunea software. „Doar configurația”: protocolul nu are o întrebare de identificare, așa că testul răspunde cu portul terminalului, fără să-l contacteze.
-   **Anulare.** Anulează o plată aprobată, cu toată suma ei. Terminalul găsește plata după identificatorul primit la plată (STAN, RRN, codul de autorizare sau numărul bonului terminalului); o plată fără el nu se poate anula prin API.
-   **Verificare automată.** Ce face aplicația cu o plată al cărei rezultat nu l-a primit: vezi [Rezultat incert](#rezultat-incert).
-   **Doar aceste operațiuni.** API-ul nu are restituiri, pre-autorizări, cashback sau bacșiș.

## Seria terminalului

Fiecare rută a unui terminal are seria lui: `/payment-terminals/{serial}/…`. Iei seria din `GET /payment-terminals`; un terminal apare în listă abia după ce aplicația l-a identificat. Seria nu ține cont de litere mari sau mici; spațiile de la capete se taie, iar orice caracter în afară de litere, cifre, `.`, `_` și `-` devine `-`. Valoarea raportată de terminal e în `rawSerial`.

| Protocol | Ce e `serial` |
| --- | --- |
| SmartPay, ePay | Seria terminalului. |
| GPE | Terminal ID-ul (TID). |
| Ingenio | Seria terminalului; dacă nu o dă, TID-ul; altfel portul. |
| Mellon, Payten, Printec, FIP11 | Portul la care e conectat: portul serial (de exemplu `COM4`) sau, prin rețea, `ip:port`, cu `:` scris `-`. |

## Fluxul unei plăți

1.  Citești terminalele: `GET /payment-terminals`. Alegi `serial`\-ul; `isDefault` arată terminalul ales implicit în aplicație, `isConnected` dacă e conectat acum.
2.  Ceri plata: `POST /payment-terminals/{serial}/sale` cu `amount` și, dacă vrei, `reference`, referința ta.
3.  Ții cererea deschisă până vine răspunsul. Clientul plătește pe terminal, iar răspunsul vine abia la final, poate după câteva minute. `transactionId` vine doar în răspuns. Dacă închizi cererea, plata nu se oprește.
4.  Citești răspunsul. `200`: plata e aprobată. `402`: refuzată, suma nu a fost încasată. `409` `terminal_uncertain`: rezultatul nu se știe, nu încasa din nou. `409` `terminal_busy`: terminalul e ocupat. `424` `terminal_not_connected`: cererea n-a ajuns la terminal, suma nu a fost încasată.
5.  Tipărești bonul fiscal cu plata cu cardul: `POST /print-fiscal-receipt`. Ruta de plată nu tipărește nimic pe casa de marcat.
6.  Dacă vânzarea se anulează: `POST /payment-terminals/{serial}/void` cu `transactionId`\-ul plății.
7.  La sfârșitul zilei, unde protocolul o are: `POST /payment-terminals/{serial}/settlement`.

### Cererea de plată

```json
{
  "amount": 12.50,
  "reference": "POS-0001"
}
```

#### Răspunsul

```json
{
  "statusCode": 200,
  "data": {
    "transactionId": "FL261109101500A1B2C3",
    "serial": "PT000001",
    "approved": true,
    "amount": 12.50,
    "currency": "RON",
    "responseCode": "00",
    "responseText": "APPROVED",
    "authCode": "A1B2C3",
    "rrn": "000000000001",
    "stan": "000001",
    "maskedPan": "************0000",
    "cardLabel": "VISA",
    "aid": "A0000000000000",
    "entryMode": "contactless",
    "pinEntered": false,
    "signatureRequired": false,
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "timestamp": "2026-11-09T08:15:04Z",
    "receiptText": null
  }
}
```

## Rutele

Exemplele folosesc seria `PT000001` și valori inventate.

GET `/payment-terminals`

### Listează terminalele

Terminalele configurate în aplicație care s-au identificat, cu starea lor. Nu ocupă niciun terminal și merge și fără casă de marcat.

| Câmp | Tip | Descriere |
| --- | --- | --- |
| `serial` | string | Seria terminalului, folosită în rute. |
| `protocol` | string | `SmartPay`, `Gpe`, `Ingenio`, `Mellon`, `Payten`, `PaytenBrd`, `Epay`, `Printec`, `PrintecDll` sau `Fip11`. |
| `connectivity` | string | `Serial` sau `Tcp`. |
| `label` | string | null | Numele dat terminalului în aplicație. |
| `isConnected` | bool | Terminalul e conectat acum. |
| `isBusy` | bool | Terminalul rulează acum o operațiune. |
| `isDefault` | bool | Terminalul ales implicit în aplicație. |

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Lista, goală dacă nu e niciun terminal. |
| 401 | — | Casa conectată nu are licență. |

GETCerere

```bash
curl -X GET 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": [
    {
      "serial": "PT000001",
      "protocol": "SmartPay",
      "connectivity": "Serial",
      "label": "Casa 1",
      "isConnected": true,
      "isBusy": false,
      "isDefault": true
    },
    {
      "serial": "COM4",
      "protocol": "Payten",
      "connectivity": "Serial",
      "label": null,
      "isConnected": false,
      "isBusy": false,
      "isDefault": false
    }
  ]
}
```

GET `/payment-terminals/{serial}`

### Detaliile unui terminal

Un terminal, cu adresa lui și tranzacțiile încă nerezolvate. Nu ocupă terminalul și merge și fără casă de marcat.

Are câmpurile din listă și, în plus:

| Câmp | Tip | Descriere |
| --- | --- | --- |
| `rawSerial` | string | null | Seria cum a raportat-o terminalul, înainte de normalizare. |
| `address` | string | Portul serial sau `ip:port`. |
| `softwareVersion` | string | null | Versiunea software a terminalului, când o dă. |
| `terminalId` | string | null | TID-ul, când terminalul l-a dat la identificare sau la ultima tranzacție. |
| `merchantId` | string | null | MID-ul, la fel. |
| `unresolvedTransactions` | string\[\] | `transactionId`\-urile încă în verificare. Cât lista nu e goală, plățile noi și închiderea de zi sunt refuzate. |

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Terminalul. |
| 401 | — | Casa conectată nu are licență. |
| 404 | `terminal_not_found` | Nicio serie configurată nu se potrivește. |

GETCerere

```bash
curl -X GET 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals/PT000001'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": {
    "serial": "PT000001",
    "rawSerial": "PT000001",
    "protocol": "SmartPay",
    "connectivity": "Serial",
    "address": "COM3",
    "label": "Casa 1",
    "isConnected": true,
    "isBusy": false,
    "isDefault": true,
    "softwareVersion": "1.0.0",
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "unresolvedTransactions": []
  }
}
```

POST `/payment-terminals/{serial}/sale`

### Plată cu cardul

Cere terminalului să încaseze `amount` lei. Răspunde după ce clientul a plătit sau plata s-a încheiat altfel. Ține cererea deschisă: `transactionId` vine doar în răspuns.

#### Cerere

| Câmp | Tip | Obligatoriu | Descriere |
| --- | --- | --- | --- |
| `amount` | decimal | Da | Suma în lei: mai mare decât 0, cel mult două zecimale, cel mult 9.999.999.999,99. |
| `reference` | string | Nu | Referința ta, salvată cu tranzacția și întoarsă când o citești. |

#### Răspuns (`data`)

| Câmp | Tip | Descriere |
| --- | --- | --- |
| `transactionId` | string | Identificatorul tranzacției, dat de aplicație: `FL`, data și ora (12 cifre) și 6 litere sau cifre. |
| `serial` | string | Seria terminalului. |
| `approved` | bool | `true` la o plată aprobată. |
| `amount` | decimal | Suma aprobată de terminal. |
| `currency` | string | Mereu `RON`. |
| `responseCode` | string | null | Codul de răspuns al terminalului sau al băncii. |
| `responseText` | string | null | Textul răspunsului, când există. |
| `authCode` | string | null | Codul de autorizare. |
| `rrn` | string | null | Numărul de referință al tranzacției (RRN). |
| `stan` | string | null | Numărul tranzacției pe terminal (STAN, numărul bonului sau al facturii terminalului). |
| `maskedPan` | string | null | Numărul cardului, mascat de terminal. |
| `cardLabel` | string | null | Tipul cardului. |
| `aid` | string | null | Identificatorul aplicației de pe cip (AID). |
| `entryMode` | string | null | `chip`, `contactless` sau `magstripe`, când terminalul o spune. |
| `pinEntered` | bool | Clientul a introdus PIN-ul. |
| `signatureRequired` | bool | Bonul cardului trebuie semnat. Doar te anunță. |
| `terminalId` | string | null | TID-ul. |
| `merchantId` | string | null | MID-ul. |
| `timestamp` | datetime | Momentul tranzacției. |
| `receiptText` | string | null | Doar GPE, când terminalul e setat să tipărească prin casă: bonul cardului formatat de terminal. Aplicația nu îl tipărește. |

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Plata e aprobată. |
| 400 | `invalid_request` | `amount` lipsă sau greșit, ori corpul nu e JSON. Nu s-a scris nicio tranzacție. |
| 401 | — | Casa conectată nu are licență. |
| 402 | `terminal_declined` | Plata e refuzată; corpul are `responseCode` și `transaction`. Suma nu a fost încasată. |
| 404 | `terminal_not_found` | Seria nu e a niciunui terminal. |
| 409 | `terminal_busy` | Terminalul e ocupat cu altă operațiune. Suma nu a fost încasată. |
| 409 | `terminal_uncertain` | Rezultatul nu se știe; corpul are `transactionId`. Nu încasa din nou. |
| 409 | `terminal_recovery_pending` | O tranzacție anterioară e nerezolvată; corpul are `transactionIds`. |
| 424 | `terminal_not_connected` | Terminalul nu e conectat, sau cererea n-a ajuns la el. Suma nu a fost încasată. |
| 424 | `cash_register_required` | Nicio casă de marcat conectată și licențiată. |
| 500 | `internal_error` | Eroare neașteptată înainte ca plata să pornească. Suma nu a fost încasată. |

POSTCerere

```bash
curl -X POST 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals/PT000001/sale' \
  -H 'Content-Type: application/json' \
  -d '{
  "amount": 12.5,
  "reference": "POS-0001"
}'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": {
    "transactionId": "FL261109101500A1B2C3",
    "serial": "PT000001",
    "approved": true,
    "amount": 12.50,
    "currency": "RON",
    "responseCode": "00",
    "responseText": "APPROVED",
    "authCode": "A1B2C3",
    "rrn": "000000000001",
    "stan": "000001",
    "maskedPan": "************0000",
    "cardLabel": "VISA",
    "aid": "A0000000000000",
    "entryMode": "contactless",
    "pinEntered": false,
    "signatureRequired": false,
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "timestamp": "2026-11-09T08:15:04Z",
    "receiptText": null
  }
}
```

402Răspuns

```json
{
  "statusCode": 402,
  "message": "The card payment was declined: DECLINED",
  "code": "terminal_declined",
  "responseCode": "05",
  "transaction": {
    "transactionId": "FL261109101500A1B2C3",
    "serial": "PT000001",
    "approved": false,
    "amount": 12.50,
    "currency": "RON",
    "responseCode": "05",
    "responseText": "DECLINED",
    "authCode": null,
    "rrn": "000000000001",
    "stan": "000001",
    "maskedPan": "************0000",
    "cardLabel": "VISA",
    "aid": null,
    "entryMode": "chip",
    "pinEntered": true,
    "signatureRequired": false,
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "timestamp": "2026-11-09T08:15:04Z",
    "receiptText": null
  }
}
```

409Răspuns

```json
{
  "statusCode": 409,
  "message": "Payment terminal PT000001 did not confirm the outcome of transaction FL261109101500A1B2C3. It is being checked; do not charge the card again.",
  "code": "terminal_uncertain",
  "transactionId": "FL261109101500A1B2C3"
}
```

POST `/payment-terminals/{serial}/void`

### Anulează o plată

Anulează pe terminal o plată aprobată, cu toată suma ei. Nu merge pe Mellon și ePay.

| Câmp | Tip | Obligatoriu | Descriere |
| --- | --- | --- | --- |
| `transactionId` | string | Da | `transactionId`\-ul din răspunsul plății. |

Răspunsul are forma răspunsului de plată, cu același `transactionId`; `stan`, `rrn` și `authCode` sunt ale anulării. După `200`, tranzacția are `state` `Voided`.

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Plata e anulată. |
| 400 | `invalid_request` | `transactionId` lipsă, ori corpul nu e JSON. |
| 401 | — | Casa conectată nu are licență. |
| 402 | `terminal_declined` | Terminalul a refuzat anularea; plata rămâne încasată. |
| 404 | `terminal_not_found` | Seria nu e a niciunui terminal. |
| 404 | `transaction_not_found` | Terminalul nu are tranzacția. |
| 409 | `terminal_busy` | Terminalul e ocupat. Plata rămâne încasată. |
| 409 | `terminal_uncertain` | Rezultatul anulării nu se știe; tranzacția rămâne `VoidPending` până se verifică. |
| 409 | `transaction_not_voidable` | Tranzacția nu e aprobată sau a fost deja anulată, protocolul nu anulează din program, ori plata nu are identificatorul după care se anulează. |
| 424 | `terminal_not_connected` | Terminalul nu e conectat, sau cererea n-a ajuns la el. Plata rămâne încasată. |
| 424 | `cash_register_required` | Nicio casă de marcat conectată și licențiată. |
| 500 | `internal_error` | Eroare neașteptată înainte ca anularea să pornească. |

POSTCerere

```bash
curl -X POST 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals/PT000001/void' \
  -H 'Content-Type: application/json' \
  -d '{
  "transactionId": "FL261109101500A1B2C3"
}'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": {
    "transactionId": "FL261109101500A1B2C3",
    "serial": "PT000001",
    "approved": true,
    "amount": 12.50,
    "currency": "RON",
    "responseCode": "00",
    "responseText": "APPROVED",
    "authCode": "A1B2C4",
    "rrn": "000000000002",
    "stan": "000002",
    "maskedPan": "************0000",
    "cardLabel": "VISA",
    "aid": null,
    "entryMode": null,
    "pinEntered": false,
    "signatureRequired": false,
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "timestamp": "2026-11-09T08:20:11Z",
    "receiptText": null
  }
}
```

409Răspuns

```json
{
  "statusCode": 409,
  "message": "Transaction FL261109101500A1B2C3 cannot be voided: it is Voided.",
  "code": "transaction_not_voidable",
  "transactionId": "FL261109101500A1B2C3"
}
```

POST `/payment-terminals/{serial}/settlement`

### Închiderea de zi

Închide lotul zilei pe terminal. E refuzată cât terminalul are o tranzacție nerezolvată, pentru că șterge jurnalul de care depinde verificarea. Fără corp.

| Câmp | Tip | Descriere |
| --- | --- | --- |
| `serial` | string | Seria terminalului. |
| `approved` | bool | Mereu `true` la `200`; un refuz răspunde `402`. |
| `responseCode` | string | null | Codul de răspuns al terminalului. |
| `responseText` | string | null | Textul răspunsului, când există. |
| `transactionCount` | int | null | Numărul de tranzacții din lot, când protocolul îl dă. |
| `totalAmount` | decimal | null | Totalul lotului, când protocolul îl dă. |
| `batchNumber` | string | null | Numărul lotului, când protocolul îl dă. |
| `terminalId` | string | null | TID-ul, când protocolul îl dă. |
| `merchantId` | string | null | MID-ul, când protocolul îl dă. |
| `timestamp` | datetime | Momentul închiderii. |

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Închiderea e făcută (pe FIP11: pornită). |
| 401 | — | Casa conectată nu are licență. |
| 402 | `terminal_declined` | Terminalul a refuzat închiderea; corpul are `responseCode`. |
| 404 | `terminal_not_found` | Seria nu e a niciunui terminal. |
| 409 | `terminal_busy` | Terminalul e ocupat. |
| 409 | `terminal_recovery_pending` | O tranzacție e încă nerezolvată; corpul are `transactionIds`. |
| 424 | `terminal_not_connected` | Terminalul nu e conectat, sau cererea n-a ajuns la el. |
| 424 | `cash_register_required` | Nicio casă de marcat conectată și licențiată. |
| 500 | `internal_error` | Eroare neașteptată, sau protocolul nu are închidere de zi din program (Mellon, Printec). |

POSTCerere

```bash
curl -X POST 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals/PT000001/settlement'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": {
    "serial": "PT000001",
    "approved": true,
    "responseCode": "00",
    "responseText": "OK",
    "transactionCount": 3,
    "totalAmount": 37.50,
    "batchNumber": "000001",
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "timestamp": "2026-11-09T20:00:12Z"
  }
}
```

POST `/payment-terminals/{serial}/test`

### Testează un terminal

Verifică legătura și returnează informațiile de identificare ale terminalului. Pentru Mellon, Payten, Printec și FIP11, răspunsul conține doar portul configurat, fără comunicare cu terminalul. Fără corp.

| Câmp | Tip | Descriere |
| --- | --- | --- |
| `rawSerial` | string | Seria raportată de terminal, sau portul. |
| `serial` | string | Seria, normalizată. |
| `softwareVersion` | string | null | Versiunea software, când o dă. |
| `terminalId` | string | null | TID-ul, când protocolul îl are la identificare. |
| `merchantId` | string | null | Niciun protocol nu îl dă la test. |

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Terminalul a răspuns. |
| 401 | — | Casa conectată nu are licență. |
| 404 | `terminal_not_found` | Seria nu e a niciunui terminal. |
| 409 | `terminal_busy` | Terminalul e ocupat. |
| 424 | `terminal_not_connected` | Terminalul nu e conectat sau nu răspunde. |
| 424 | `cash_register_required` | Nicio casă de marcat conectată și licențiată. |
| 500 | `internal_error` | Eroare neașteptată. |

POSTCerere

```bash
curl -X POST 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals/PT000001/test'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": {
    "rawSerial": "PT000001",
    "serial": "PT000001",
    "softwareVersion": "1.0.0",
    "terminalId": null,
    "merchantId": null
  }
}
```

GET `/payment-terminals/{serial}/transactions/{transactionId}`

### Citește o tranzacție

O tranzacție a terminalului, cu starea ei. Merge oricând, și în timpul unei plăți, și fără casă de marcat.

Are câmpurile răspunsului de plată, fără `approved`, `aid`, `pinEntered`, `timestamp` și `receiptText`, plus:

| Câmp | Tip | Descriere |
| --- | --- | --- |
| `state` | string | `Pending`, `Approved`, `Declined`, `Uncertain`, `NotPerformed`, `VoidPending` sau `Voided`; vezi [Stările tranzacției](#starile-tranzactiei). |
| `amount` | decimal | Suma cerută. |
| `approvedAmount` | decimal | null | Suma aprobată, după aprobare. |
| `reference` | string | null | Referința trimisă la plată. |
| `createdAt` | datetime | Când a fost scrisă tranzacția (UTC). |
| `updatedAt` | datetime | Ultima schimbare (UTC). |

#### Coduri de răspuns

| Status | `code` | Când |
| --- | --- | --- |
| 200 | — | Tranzacția. |
| 401 | — | Casa conectată nu are licență. |
| 404 | `terminal_not_found` | Seria nu e a niciunui terminal. |
| 404 | `transaction_not_found` | Terminalul nu are tranzacția (cele încheiate se șterg după 180 de zile). |

GETCerere

```bash
curl -X GET 'https://localhost.fiscallink.ro:65401/api/v2/payment-terminals/PT000001/transactions/FL261109101500A1B2C3'
```

200Răspuns

```json
{
  "statusCode": 200,
  "data": {
    "transactionId": "FL261109101500A1B2C3",
    "serial": "PT000001",
    "state": "Approved",
    "amount": 12.50,
    "approvedAmount": 12.50,
    "currency": "RON",
    "reference": "POS-0001",
    "responseCode": "00",
    "responseText": "APPROVED",
    "authCode": "A1B2C3",
    "rrn": "000000000001",
    "stan": "000001",
    "maskedPan": "************0000",
    "cardLabel": "VISA",
    "entryMode": "contactless",
    "signatureRequired": false,
    "terminalId": "TID00001",
    "merchantId": "MID000000001",
    "createdAt": "2026-11-09T08:15:00Z",
    "updatedAt": "2026-11-09T08:15:04Z"
  }
}
```

404Răspuns

```json
{
  "statusCode": 404,
  "message": "Payment terminal PT000001 has no transaction FL000000000000XXXXXX.",
  "code": "transaction_not_found",
  "transactionId": "FL000000000000XXXXXX"
}
```

## Stările tranzacției

Aplicația salvează tranzacția înainte de a trimite cererea la terminal, apoi actualizează rezultatul. Astfel, poți verifica o plată și după repornire sau după pierderea răspunsului. Citește câmpul `state` din `GET /payment-terminals/{serial}/transactions/{transactionId}`.

| `state` | Ce înseamnă | Nerezolvată |
| --- | --- | --- |
| `Pending` | Plata a început și aplicația nu a terminat-o (de exemplu, s-a oprit în timpul plății). | Da |
| `Approved` | Aprobată: banii s-au luat de pe card. | Nu |
| `Declined` | Refuzată de bancă, de card sau de client. Suma nu a fost încasată. | Nu |
| `Uncertain` | Terminalul a primit cererea, dar nu a venit un răspuns valid: suma poate fi deja încasată. | Da |
| `NotPerformed` | Cererea n-a ajuns la terminal, terminalul a refuzat-o fiindcă era ocupat, sau verificarea a aflat că plata nu s-a făcut. Suma nu a fost încasată. | Nu |
| `VoidPending` | Anularea a pornit și rezultatul ei nu se știe. | Da |
| `Voided` | Anulată: suma a fost restituită pe card. Și o plată incertă pe care verificarea a găsit-o aprobată și a anulat-o. | Nu |

Tranzacțiile încheiate se păstrează 180 de zile; cele nerezolvate nu se șterg.

## Rezultat incert

Dacă se întrerupe conexiunea sau terminalul nu confirmă rezultatul, aplicația marchează plata ca incertă. Cererea poate fi ajuns la terminal și suma poate fi deja încasată.

-   Plata (sau anularea) răspunde `409` `terminal_uncertain`, cu `transactionId`. **Nu încasa din nou** aceeași vânzare.
-   Cât terminalul are o tranzacție nerezolvată (`Pending`, `Uncertain` sau `VoidPending`), o plată nouă și închiderea de zi pe acel terminal răspund `409` `terminal_recovery_pending`, cu lista `transactionIds`. Închiderea de zi e refuzată fiindcă șterge jurnalul terminalului, singura dovadă a ce s-a întâmplat.
-   **SmartPay și GPE:** aplicația întreabă singură terminalul, când acesta se conectează și apoi periodic, tot mai rar, până la o dată la 10 minute, fără să renunțe. O plată găsită aprobată e anulată, cum cer protocoalele; una care nu s-a făcut devine `NotPerformed`. O anulare cu rezultat necunoscut se verifică la fel.
-   **Ingenio, Mellon, Payten, ePay, Printec și FIP11:** aplicația nu poate citi jurnalul acestor terminale, deci nu poate rezolva singură tranzacția. Ea rămâne nerezolvată, iar terminalul refuză plăți noi și închiderea de zi.
-   Verificarea rulează doar cât terminalul e conectat, deci cât e conectată și casa de marcat.
-   Urmărești rezultatul cu `GET /payment-terminals/{serial}/transactions/{transactionId}`; `unresolvedTransactions` din `GET /payment-terminals/{serial}` arată ce se verifică încă.

```json
{
  "statusCode": 409,
  "message": "Payment terminal PT000001 did not confirm the outcome of transaction FL261109101500A1B2C3. It is being checked; do not charge the card again.",
  "code": "terminal_uncertain",
  "transactionId": "FL261109101500A1B2C3"
}
```

```json
{
  "statusCode": 409,
  "message": "Payment terminal PT000001 is still checking an earlier transaction (FL261109101500A1B2C3). Try again once it is resolved.",
  "code": "terminal_recovery_pending",
  "transactionIds": ["FL261109101500A1B2C3"]
}
```

## O operațiune pe terminal

-   **Fără coadă.** Dacă terminalul este ocupat, o cerere nouă primește imediat `409` cu `terminal_busy`. Așteaptă rezultatul primei operațiuni înainte de a trimite alta. Terminalul este eliberat înainte de transmiterea răspunsului.
-   **Terminalele nu se așteaptă între ele.** Două terminale încasează în același timp, iar casa de marcat tipărește în timp ce un terminal încasează.
-   **Citirile nu ocupă terminalul.** Lista, detaliile și o tranzacție răspund oricând, și în timpul unei plăți. `isBusy` arată dacă terminalul rulează acum o operațiune.
-   **Fără \`Idempotency-Key\`.** Rutele terminalelor nu citesc antetul: fiecare `POST …/sale` e o plată nouă, cu alt `transactionId`. Nu retrimite o plată al cărei răspuns nu l-ai primit: suma poate fi deja încasată.

```json
{
  "statusCode": 409,
  "message": "Payment terminal PT000001 is busy with another operation. Wait for it to finish, then try again.",
  "code": "terminal_busy"
}
```

## Erori și coduri

Verifică întotdeauna câmpul `code`, nu doar statusul HTTP. De exemplu, `409` cu `terminal_busy` permite o încercare ulterioară, în timp ce `terminal_uncertain` cere verificarea plății înainte de o nouă încasare.

| Status | `code` | Câmpuri în plus | Când | Ce faci |
| --- | --- | --- | --- | --- |
| 400 | `invalid_request` | — | Corp lipsă sau JSON greșit; `amount` lipsă, zero, negativ, cu mai mult de două zecimale sau peste 9.999.999.999,99; `transactionId` lipsă la anulare. | Corectezi cererea. Nu s-a scris nicio tranzacție. |
| 401 | — | — | Casa conectată nu are licență. | Activezi licența casei. |
| 402 | `terminal_declined` | `responseCode`, `transaction` | Banca, cardul sau clientul a refuzat plata; sau terminalul a refuzat anularea ori închiderea de zi. | La plată suma nu a fost încasată. La anulare plata rămâne încasată. |
| 404 | `terminal_not_found` | — | Nicio serie configurată nu se potrivește. | Citești din nou lista. |
| 404 | `transaction_not_found` | `transactionId` | Terminalul nu are tranzacția. | Verifici `transactionId`\-ul și seria. |
| 409 | `terminal_busy` | — | Terminalul rulează altă operațiune. | Încerci după ce se termină. Suma nu a fost încasată. |
| 409 | `terminal_uncertain` | `transactionId` | Rezultatul plății sau al anulării nu se știe. | Nu încasezi din nou. Urmărești tranzacția. |
| 409 | `terminal_recovery_pending` | `transactionIds` | O tranzacție anterioară e încă nerezolvată. | Aștepți să se rezolve. |
| 409 | `transaction_not_voidable` | `transactionId` | Tranzacția nu e aprobată (refuzată, deja anulată, nerezolvată), protocolul nu anulează din program, sau plata nu are identificatorul după care se anulează. | Nu se poate anula prin API. |
| 424 | `terminal_not_connected` | — | Terminalul nu e conectat, sau cererea nu a ajuns la el. | Suma nu a fost încasată. Verifici cablul sau rețeaua. |
| 424 | `cash_register_required` | — | Nicio casă de marcat conectată și licențiată. | Conectezi casa de marcat. |
| 500 | `internal_error` | — | O eroare neașteptată în aplicație, înainte ca plata sau anularea să pornească; sau închiderea de zi pe Mellon și Printec. | Suma nu a fost încasată. |

Codurile `serial_in_use`, `port_in_use`, `identity_changed`, `unresolved_transactions`, `invalid_configuration` și `protocol_not_available` țin de adăugarea unui terminal în aplicație; rutele de aici nu le întorc.

```json
{
  "statusCode": 424,
  "message": "Payment terminals work only while a cash register is connected.",
  "code": "cash_register_required"
}
```

## Diferențe între terminale

Toate terminalele răspund cu aceleași câmpuri. Ce completează fiecare depinde de protocol; un câmp pe care protocolul nu îl dă e `null` (sau `false`), iar un terminal anume poate lăsa goale și câmpuri din listă.

| Protocol | Câmpuri completate la plată |
| --- | --- |
| SmartPay | `responseCode`, `responseText`, `authCode`, `rrn`, `stan`, `maskedPan`, `cardLabel`, `aid`, `entryMode`, `pinEntered`, `signatureRequired`, `terminalId`, `merchantId` |
| GPE | `responseCode`, `responseText`, `authCode`, `stan`, `maskedPan`, `cardLabel`, `aid`, `signatureRequired`, `terminalId`, `receiptText` |
| Ingenio | `responseCode`, `responseText`, `authCode`, `rrn`, `stan`, `maskedPan`, `cardLabel`, `terminalId`, `merchantId` |
| Mellon | `responseCode`, `responseText`, `authCode`, `stan`, `terminalId` |
| Payten | `responseCode`, `responseText`, `authCode`, `rrn`, `stan`, `maskedPan`, `cardLabel`, `entryMode`, `pinEntered`, `terminalId`, `merchantId` |
| ePay | `responseCode`, `responseText`, `authCode`, `rrn`, `stan`, `maskedPan`, `cardLabel`, `aid`, `entryMode`, `terminalId`, `merchantId` |
| Printec | `responseCode`, `responseText`, `authCode`, `rrn`, `stan`, `maskedPan`, `pinEntered`, `terminalId` |
| FIP11 | `responseCode`, `responseText`, `authCode`, `stan`, `maskedPan`, `cardLabel` |

| Protocol | Anularea se face după | Închiderea de zi întoarce |
| --- | --- | --- |
| SmartPay | STAN | `transactionCount`, `totalAmount`, `batchNumber` |
| GPE | codul de autorizare (reversare) | `transactionCount`, `totalAmount` |
| Ingenio | RRN | `batchNumber` |
| Mellon | nu anulează din program | nu are |
| Payten | numărul tranzacției pe terminal | `transactionCount`, `totalAmount`, `batchNumber` |
| ePay | nu anulează din program | doar confirmarea |
| Printec | numărul bonului terminalului | nu are |
| FIP11 | numărul facturii terminalului | doar că a pornit |

-   **\`entryMode\`** e `chip`, `contactless` sau `magstripe`, când terminalul o spune.
-   **\`signatureRequired\`** (SmartPay, GPE) te anunță doar că bonul cardului trebuie semnat; verificarea semnăturii revine casierului.
-   **\`receiptText\`** (doar GPE, când terminalul e setat să tipărească prin casă): bonul cardului, formatat de terminal pe linii de 24 de caractere, cu marcajele lui de formatare. Aplicația nu îl tipărește.
-   **Suma aprobată.** `amount` din răspunsul plății e suma aprobată de terminal; în tranzacție e `approvedAmount`. Pe SmartPay, o aprobare pe altă sumă decât cea cerută e tratată ca rezultat incert.
