L'API pubblica di 4invoices invia i dati di fatturazione dal tuo sistema a 4invoices e, in Romania, porta la fattura fino in fondo nell'SPV dell'ANAF. Un solo host di produzione, un flusso client_credentials, scritture fiscali idempotenti e un unico involucro di errore su ogni rotta.
https://api.4invoices.net/v1 · Specifica OpenAPI (senza chiave)
Cos'è l'API 4invoices
- L’API pubblica di 4invoices è REST su HTTPS, con JSON in ingresso e in uscita.
- Un unico host di produzione: https://api.4invoices.net/v1. Non esiste un host di test separato.
- La specifica OpenAPI 3.1 è pubblica e accessibile senza chiave: importala direttamente in Postman o in un generatore di client.
- Ogni risposta include l’header X-Request-Id; indicalo quando chiedi assistenza.
- Ogni scrittura fiscale è idempotente: un nuovo tentativo dopo un timeout non emette mai un secondo documento.
- Per la Romania, l’intero ciclo e-Factura di ANAF tramite API: controllo preliminare, invio, tracciamento, risposta firmata e casella in entrata. Nessun concorrente rumeno lo offre.
- SmartBill non ha alcun endpoint per l’invio al sistema nazionale di fatturazione elettronica (in Romania, ANAF) — l’invio allo SPV è solo un’impostazione dell’account nell’interfaccia — né alcun endpoint per elencare le fatture.
- Oblio può creare clienti e prodotti solo come effetto collaterale dell’emissione di un documento, non come risorse a sé stanti.
- Generazione dell’XML UBL / CIUS-RO (Romania) senza invio: ottieni il documento legale come file senza toccare lo SPV rumeno.
- Idempotenza sulle scritture fiscali: un nuovo tentativo dopo un timeout non produce una seconda fattura né consuma un secondo numero.
- Una casella delle fatture dei fornitori leggibile via codice: la base per una riconciliazione automatizzata.
La prima fattura in 6 passi
- Genera una credenziale nel pannello, in Fatturazione → Impostazioni → Accesso API, e copia il client_secret — viene mostrato una sola volta.
- Completa il profilo aziendale e attiva almeno una serie di tipo fiscale, altrimenti l'emissione restituisce issuer_profile_incomplete o no_series_configured.
- Genera una credenziale nel pannello, in Fatturazione → Impostazioni → Accesso API, e copia il client_secret — viene mostrato una sola volta.
- Completa il profilo aziendale e attiva almeno una serie di tipo fiscale, altrimenti l'emissione restituisce issuer_profile_incomplete o no_series_configured.
- Scambia la credenziale con un token: POST /v1/auth/token con grant_type client_credentials. Il token dura un'ora — mettilo in cache, non richiederne uno per ogni chiamata.
- Emetti la fattura: POST /v1/invoices con header Idempotency-Key e vat_rate in percentuale (21, non 0.21).
- Per il mercato rumeno, esegui il preflight CIUS-RO con POST /v1/invoices/{id}/efactura/validate, poi invia all'ANAF con POST /v1/invoices/{id}/efactura/submit.
- Segui lo stato con GET /v1/efactura/submissions/{id} finché non arriva ad accepted o rejected.
bash
# 1 — token (1 ora, da mettere in cache)
ACCESS_TOKEN=$(curl -sS -X POST https://api.4invoices.net/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{"grant_type":"client_credentials","client_id":"cli_XXX","client_secret":"sec_XXX"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
# 2 — emetti la fattura (Idempotency-Key è OBBLIGATORIO)
curl -sS -X POST https://api.4invoices.net/v1/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-10457-invoice' \
-d '{"buyer":{"name":"Exemplu Comert SRL","tax_id":"RO12345678","county":"Cluj","city":"Cluj-Napoca","country":"RO"},
"lines":[{"description":"Abonament SaaS","quantity":1,"unit_price":400,"vat_rate":21}]}'
# 3 — preflight CIUS-RO, then send to ANAF
curl -sS -X POST https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/validate \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl -sS -X POST https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/submit \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Idempotency-Key: efactura-inv_01J7ABC-1'javascript
const BASE = "https://api.4invoices.net/v1";
const token = await fetch(`${BASE}/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.FOURINVOICES_CLIENT_ID,
client_secret: process.env.FOURINVOICES_CLIENT_SECRET,
}),
}).then((r) => r.json()).then((d) => d.access_token);
const invoice = await fetch(`${BASE}/invoices`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-${orderId}-invoice`,
},
body: JSON.stringify({
buyer: { name: "Exemplu Comert SRL", tax_id: "RO12345678", county: "Cluj", city: "Cluj-Napoca", country: "RO" },
lines: [{ description: "Abonament SaaS", quantity: 1, unit_price: 400, vat_rate: 21 }],
currency: "RON",
metadata: { order_id: String(orderId) },
}),
}).then((r) => r.json());
console.log(invoice.number, invoice.totals); // "FCT-000123" { net, vat, gross }python
import os, uuid, requests
BASE = "https://api.4invoices.net/v1"
token = requests.post(f"{BASE}/auth/token", json={
"grant_type": "client_credentials",
"client_id": os.environ["FOURINVOICES_CLIENT_ID"],
"client_secret": os.environ["FOURINVOICES_CLIENT_SECRET"],
}, timeout=30).json()["access_token"]
headers = {
"Authorization": f"Bearer {token}",
# Una chiave per OGNI operazione LOGICA — riusala al retry, non rigenerarla mai.
"Idempotency-Key": f"order-{order_id}-invoice",
}
invoice = requests.post(f"{BASE}/invoices", headers=headers, json={
"buyer": {"name": "Exemplu Comert SRL", "tax_id": "RO12345678",
"county": "Cluj", "city": "Cluj-Napoca", "country": "RO"},
"lines": [{"description": "Abonament SaaS", "quantity": 1,
"unit_price": 400, "vat_rate": 21}], # percentuale, non 0.21
"currency": "RON",
}, timeout=60).json()
print(invoice["number"], invoice["totals"])Autenticazione
- Il flusso è client_credentials: scambi client_id + client_secret con un access_token di tipo Bearer.
- Il token scade dopo 3600 secondi. Rinnovalo prima della scadenza, non a ogni chiamata: POST /auth/token ha un proprio limite di frequenza per IP.
- Il client_secret viene memorizzato solo come hash e non può essere recuperato. Se lo perdi, crei una nuova credenziale e revochi quella vecchia.
- Solo server-to-server. CORS non consente alcuna origine browser: dal JavaScript di una pagina il secret finirebbe nelle mani del visitatore.
- Gli scope vengono assegnati per credenziale. Crea una credenziale separata per ogni sistema e assegna a ciascuna solo gli scope minimi necessari.
Fatturazione → Impostazioni → Accesso API
bash
curl -sS -X POST https://api.4invoices.net/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "cli_XXXXXXXXXXXX",
"client_secret": "sec_XXXXXXXXXXXXXXXXXXXX"
}'
# → {"access_token":"eyJ...","token_type":"Bearer","expires_in":3600,
# "scope":"invoices:read invoices:write efactura:submit"}javascript
// Metti in cache il token; non generarne uno per ogni richiesta.
let cached = { token: "", expiresAt: 0 };
async function getToken() {
if (cached.token && Date.now() < cached.expiresAt - 60_000) return cached.token;
const res = await fetch("https://api.4invoices.net/v1/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.FOURINVOICES_CLIENT_ID,
client_secret: process.env.FOURINVOICES_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`token ${res.status}`);
const data = await res.json();
cached = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
return cached.token;
}Le aliquote IVA sono percentuali
vat_rate: 21 ✓ · vat_rate: 0.19 ✗ · vat_rate: 1 ✗- vat_rate è una PERCENTUALE: 21 significa 21%.
- I valori nell’intervallo (0, 1] vengono rifiutati: 0.19 è una frazione e 1 è ambiguo tra 1% e 100%.
- 0 è valido e indica esenzione o inversione contabile (reverse charge); inserisci la dicitura di legge in notes.
- unit_price è il prezzo unitario NETTO quando l’emittente è soggetto IVA.
- Ti servono i totali prima di confermare? POST /v1/invoices/preview-totals esegue lo stesso calcolo senza consumare un numero fiscale.
Idempotenza sulle scritture fiscali
- L’header Idempotency-Key è OBBLIGATORIO su POST /invoices, POST /invoices/{id}/storno e POST /invoices/{id}/efactura/submit.
- Le chiavi vengono conservate per 24 ore. Reinviare la stessa chiave con lo stesso body restituisce il risultato originale e imposta Idempotency-Replayed: true.
- La stessa chiave con un body diverso restituisce 409 idempotency_key_reuse: un documento fiscale già emesso non viene mai sovrascritto.
- Una chiave per operazione LOGICA, non per tentativo HTTP: order-10457-invoice, non un nuovo UUID a ogni tentativo.
- Il motivo è legale, non tecnico: un numero fiscale consumato e una fattura depositata nel sistema nazionale di fatturazione elettronica non possono essere ritirati.
bash
curl -sS -X POST https://api.4invoices.net/v1/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-10457-invoice' \
-d '{
"buyer": {
"name": "Exemplu Comert SRL",
"tax_id": "RO12345678",
"address": "Str. Fabricii 12",
"city": "Cluj-Napoca",
"county": "Cluj",
"country": "RO",
"email": "[email protected]"
},
"lines": [
{ "description": "Abonament SaaS, iulie", "quantity": 1, "unit_price": 400, "vat_rate": 21, "unit": "buc" },
{ "description": "Implementare", "quantity": 6, "unit_price": 250, "vat_rate": 21, "unit": "ora" }
],
"currency": "RON",
"document_kind": "factura",
"due_date": "2026-08-15",
"metadata": { "order_id": "10457" }
}'Tutti gli endpoint
| Metodo | Endpoint | Scope | Idempotency-Key | Richieste / minuto |
|---|---|---|---|---|
| POST | /auth/token Scambia le credenziali con un token di accesso | — | — | 120 |
| GET | /me Tenant, scope e limiti attuali | invoices:read | — | 120 |
| POST | /invoices Emetti una fattura o una proforma | invoices:write | obbligatorio | 30 |
| GET | /invoices Elenca le fatture (paginazione a cursore) | invoices:read | — | 120 |
| POST | /invoices/preview-totals Calcola i totali senza creare nulla | invoices:read | — | 120 |
| GET | /invoices/{invoice_id} Recupera una fattura | invoices:read | — | 120 |
| GET | /invoices/{invoice_id}/pdf Scarica il PDF della fattura | invoices:read | — | 120 |
| GET | /invoices/{invoice_id}/xml Scarica l'XML UBL / CIUS-RO (Romania) | invoices:read | — | 120 |
| POST | /invoices/{invoice_id}/storno Emetti una nota di credito che storna questa fattura | invoices:write | obbligatorio | 30 |
| POST | /invoices/{invoice_id}/payments Segna la fattura come pagata | invoices:write | — | 30 |
| DELETE | /invoices/{invoice_id}/payments Segna la fattura come non pagata | invoices:write | — | 30 |
| GET | /clients Elenca i clienti | clients:read | — | 120 |
| POST | /clients Crea o aggiorna un cliente | clients:write | — | 30 |
| GET | /lookup/company/{tax_id} Verifica un CUI rumeno nel registro ANAF | clients:read | — | 120 |
| GET | /series Elenca le serie di numerazione | invoices:read | — | 120 |
| POST | /invoices/{invoice_id}/efactura/validate Preflight CIUS-RO (Romania) — convalida senza inviare | efactura:read | — | 120 |
| POST | /invoices/{invoice_id}/efactura/submit Invia la fattura ad ANAF (SPV, Romania) | efactura:submit | obbligatorio | 10 |
| GET | /efactura/submissions Elenca gli invii ad ANAF (Romania) | efactura:read | — | 120 |
| GET | /efactura/submissions/{submission_id} Recupera un invio | efactura:read | — | 120 |
| POST | /efactura/submissions/{submission_id}/refresh Chiedi ad ANAF (Romania) lo stato attuale | efactura:read | — | 10 |
| GET | /efactura/submissions/{submission_id}/download Scarica l'archivio della risposta firmata | efactura:read | — | 120 |
| GET | /efactura/inbox Fatture passive ricevute tramite SPV (Romania) | efactura:read | — | 120 |
| GET | /efactura/inbox/{inbound_id} Recupera una fattura ricevuta | efactura:read | — | 120 |
| GET | /efactura/inbox/{inbound_id}/download Scarica l'archivio della fattura ricevuta | efactura:read | — | 120 |
| POST | /efactura/inbox/sync Scarica i nuovi messaggi SPV (ANAF, Romania) nella casella | efactura:read | — | 10 |
25 endpoints · https://api.4invoices.net/v1/openapi.json
Scope
- invoices:read
- invoices:write
- clients:read
- clients:write
- efactura:read
- efactura:submit
Il percorso ANAF e-Factura (Romania)
- Il vero elemento distintivo: per la Romania, l’intero ciclo e-Factura di ANAF passa dall’API, non solo la generazione del documento.
- POST /invoices/{id}/efactura/validate esegue, per il mercato rumeno, il controllo preliminare CIUS-RO e restituisce esattamente i campi che ANAF rifiuterebbe, senza inviare nulla.
- GET /invoices/{id}/xml genera l’XML UBL / CIUS-RO (il profilo rumeno) senza alcun invio: utile per il tuo archivio o per un audit interno.
- POST /invoices/{id}/efactura/submit deposita la fattura nello SPV (ANAF, Romania). L’operazione è irreversibile: una seconda copia della stessa fattura non può essere ritirata dallo SPV rumeno.
- Il ciclo di vita è normalizzato: not_sent → validating → queued → sent → processing → accepted | rejected | error. accepted significa trasmessa a norma di legge; error è un errore di trasporto dal nostro lato e si può ritentare senza rischi.
- GET /efactura/submissions/{id}/download scarica, per il mercato rumeno, l’archivio con la risposta firmata di ANAF.
- Esiste anche il lato in entrata: POST /efactura/inbox/sync recupera i nuovi messaggi dello SPV (ANAF, Romania) e GET /efactura/inbox elenca le fatture che ti hanno inviato i fornitori.
bash
# CIUS-RO preflight. Sends NOTHING to ANAF.
curl -sS -X POST "https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/validate?include_xml=true" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# → {"invoice_id":"inv_01J7ABC","valid":false,
# "errors":["buyer.county is required for RO buyers"],
# "xml_base64":"PD94bWwg..."}bash
curl -sS -X POST https://api.4invoices.net/v1/invoices/inv_01J7ABC/efactura/submit \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Idempotency-Key: efactura-inv_01J7ABC-1'
# → {"submission_id":"sub_01J7XYZ","status":"queued"}python
import time, requests
BASE = "https://api.4invoices.net/v1"
TERMINAL = {"accepted", "rejected"}
H = {"Authorization": f"Bearer {token}"}
def wait_for_anaf(submission_id, timeout=900):
deadline = time.time() + timeout
delay = 15
while time.time() < deadline:
r = requests.get(f"{BASE}/efactura/submissions/{submission_id}", headers=H, timeout=30)
r.raise_for_status()
status = r.json()["status"]
if status in TERMINAL:
return r.json()
# /refresh asks ANAF directly and shares the 10/min submit bucket.
requests.post(f"{BASE}/efactura/submissions/{submission_id}/refresh", headers=H, timeout=30)
time.sleep(delay)
delay = min(delay * 2, 120)
raise TimeoutError(submission_id)Errori
- Ogni risposta 4xx e 5xx usa lo stesso involucro: { "error": { type, code, message, param?, doc_url, request_id, errors? } }.
- type è la classe generica su cui ramifichi la logica («posso ritentare?»), mentre code è il motivo stabile da mappare nel tuo sistema.
- param è il percorso puntato del campo problematico, per esempio lines.0.vat_rate.
- errors[] compare quando una singola richiesta presenta problemi su più di un campo.
- request_id viene registrato dal nostro lato. Indicalo in ogni ticket di assistenza: identifica la richiesta esatta.
- Un 404 viene restituito anche per un id che appartiene a un altro tenant: non confermiamo mai l’esistenza dei dati di qualcun altro.
json
{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "Datele trimise nu sunt valide.",
"param": "lines.0.vat_rate",
"doc_url": "https://4invoices.net/ro/api-dezvoltatori#errors",
"request_id": "req_8a893f2f33d4472da8d6e144",
"errors": [
{ "param": "lines.0.vat_rate", "message": "VAT rate must be a percent (21), not a fraction", "type": "value_error" }
]
}
}| Stato | Tipo | Codice |
|---|---|---|
| 401 | authentication_error | missing_bearer_token |
| 401 | authentication_error | invalid_client |
| 401 | authentication_error | token_expired |
| 401 | authentication_error | invalid_token |
| 401 | authentication_error | unauthorized |
| 403 | permission_error | insufficient_scope |
| 403 | permission_error | forbidden |
| 404 | not_found_error | not_found |
| 409 | conflict_error | idempotency_key_reuse |
| 409 | conflict_error | idempotency_in_progress |
| 409 | conflict_error | no_series_configured |
| 422 | invalid_request_error | validation_failed |
| 422 | invalid_request_error | issuer_profile_incomplete |
| 422 | invalid_request_error | cius_ro_incomplete |
| 422 | invalid_request_error | cannot_storno_draft |
| 422 | invalid_request_error | nothing_to_storno |
| 422 | invalid_request_error | invalid_json_number |
| 429 | rate_limit_error | rate_limit_exceeded |
| 502 | upstream_error | upstream_error |
| 503 | service_unavailable_error | service_unavailable |
Limiti di frequenza
| Bucket | Richieste / minuto | Significato |
|---|---|---|
| read | 120 | GET *, POST /invoices/preview-totals, POST /invoices/{id}/efactura/validate |
| write | 30 | POST /invoices, POST /invoices/{id}/storno, POST|DELETE /invoices/{id}/payments, POST /clients |
| submit | 10 | POST /invoices/{id}/efactura/submit, POST /efactura/submissions/{id}/refresh, POST /efactura/inbox/sync |
- I limiti sono per credenziale e al minuto: 120 per le letture, 30 per le scritture, 10 per le operazioni di fatturazione elettronica (submit, refresh, inbox/sync).
- X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset compaiono in OGNI risposta, 429 compresa.
- Un 429 include anche Retry-After in secondi. Rispettalo; non ritentare in un ciclo stretto.
- Il limite di 10/min esiste perché in Romania ANAF stessa consente circa una chiamata all’elenco messaggi al minuto per CIF.
- Esegui /efactura/inbox/sync con un cron, non con un polling aggressivo.
- Ti servono limiti più alti? Chiedili tramite Contatti indicando il volume giornaliero previsto.
X-RateLimit-Limit · X-RateLimit-Remaining · X-RateLimit-Reset · Retry-After
http
HTTP/2 429
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1785312000
Retry-After: 17
Content-Type: application/json
{"error":{"type":"rate_limit_error","code":"rate_limit_exceeded",
"message":"...","doc_url":"https://4invoices.net/ro/api-dezvoltatori#errors",
"request_id":"req_..."}}javascript
// Rispetta Retry-After; non ritentare mai un 4xx diverso da 429.
async function call(url, init, attempt = 0) {
const res = await fetch(url, init);
if (res.status === 429 || res.status >= 500) {
if (attempt >= 5) return res;
const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return call(url, init, attempt + 1);
}
return res;
}
// Un retry di scrittura DEVE riusare lo STESSO Idempotency-Key, altrimenti emetti
// un secondo documento fiscale.Guide per argomento
Ogni argomento ha la sua pagina, con endpoint, esempi ed errori specifici.
- Autenticazione all'API 4invoicesScambi client_id e client_secret con un token Bearer valido un'ora, solo server-to-server.
- Emettere fatture tramite APIPOST /invoices con Idempotency-Key, IVA in percentuale, più elenco, storno e segna come pagata.
- Clienti e verifica nel registro ANAF (Romania)Un inserimento o aggiornamento deduplicato su tax_id e una ricerca nel registro ANAF per le aziende rumene, perché l'e-Factura non venga respinta.
- e-Factura ANAF (Romania) tramite APIPer la Romania: preflight CIUS-RO, invio all'SPV, stato normalizzato, risposta firmata e inbox delle fatture ricevute.
- Serie di numerazione nell'APISenza una serie attiva di tipo fiscale, l'emissione restituisce 409 no_series_configured.
- Codici di errore e involucro dell'erroreUna sola forma su ogni rotta: type, code, message, param, doc_url, request_id ed errors.
- Limiti di frequenza e retry120 letture, 30 scritture e 10 operazioni di fatturazione elettronica al minuto per credenziale, con header X-RateLimit-*.
Errori frequenti
- Inviare l’IVA come frazione (0.19) o come 1. Entrambi vengono rifiutati; invia 19 o 21.
- Generare un token per ogni richiesta. Il token dura un’ora; mettilo in cache, altrimenti raggiungerai il limite per IP dell’endpoint del token.
- Generare una nuova chiave di idempotenza a ogni tentativo: esattamente lo scenario che produce fatture duplicate.
- Chiamare l’API dal JavaScript del browser. CORS blocca ogni origine e il secret sarebbe comunque esposto.
- Depositare nel sistema nazionale di fatturazione elettronica senza il controllo preliminare (CIUS-RO in Romania). Un invio errato non può essere ritirato.
- Interrogare /efactura/submissions/{id}/refresh più di 10 volte al minuto: quel limite è condiviso con submit.
- Emettere prima che il profilo aziendale sia completo e che esista una serie fiscale attiva.
Domande e risposte per sviluppatori
Come ottengo le credenziali per l'API 4invoices?
Nel pannello, in Fatturazione → Impostazioni → Accesso API. Ricevi client_id e client_secret; il secret viene mostrato una sola volta ed è conservato solo come hash.
Quanto dura l'access_token?
3600 secondi, cioè un'ora. Mettilo in cache e rinnovalo prima della scadenza — l'endpoint del token ha un limite per IP.
Come invio l'aliquota IVA?
Come percentuale: 21 per il 21%. I valori in (0, 1] vengono rifiutati, quindi 0.19 e 1 danno errore.
Quali chiamate richiedono un Idempotency-Key?
POST /invoices, POST /invoices/{id}/storno e POST /invoices/{id}/efactura/submit. Le chiavi restano valide 24 ore e un replay restituisce il documento originale con Idempotency-Replayed: true.
Posso inviare una fattura all'e-Factura ANAF (Romania) direttamente dall'API?
Sì, per il mercato rumeno: preflight CIUS-RO con /efactura/validate, invio con /efactura/submit, stato normalizzato su /efactura/submissions/{id} e download della risposta firmata dall'ANAF.
Quali sono i limiti di frequenza?
Per credenziale, al minuto: 120 letture, 30 scritture e 10 operazioni di fatturazione elettronica. Ogni risposta porta le intestazioni X-RateLimit-*, e un 429 aggiunge Retry-After.
Posso chiamare l'API dal frontend?
No. È esclusivamente server-to-server: nessun origin di browser passa CORS e un client_secret non va in codice che raggiunge il visitatore.
Perché ricevo issuer_profile_incomplete sulla prima fattura?
Il profilo aziendale è incompleto o non esiste una serie attiva di tipo fiscale. Completa entrambi nel pannello; l'API non produce un documento fiscale non valido.